Dgraph Firebase Authentication - Role Based Access Control
Tags: dgraph, firebase, authentication, graphql
Published: 2021-06-04 02:24:18

**Note:** Since version **21.03** of Dgraph, you can login with Firebase and you don't need a firebase function!

There is no official documentation on using Firebase with Dgraph, 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, and be as thorough as possible. There will be the custom claims version for advanced users, and standard claims if you just want to get going.

You will need a Firebase Project regardless.

## Custom Claims Version (with Firebase Functions)
___

**Step 1 - Create a Firebase Project**

https://firebase.google.com/

Very self explanatory.  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 Dgraph Schema, add Firebase Project ID**

Create an account at cloud.dgraph.com, and add this at the bottom of your schema with your Firebase Project ID:

```typescript
# 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"]}
```
You technically don't need a namespace, but you may decide you prefer your own [custom claims](https://dgraph.io/docs/graphql/authorization/authorization-overview/) later.

**Step 3 - Create Firebase Function for Custom Claims** 

Remeber that you **don't** actually need to create a firebase function to use RBAC. This is just in case you want to use custom claims. Everything you need is already in standard claims.

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

Create a **config.ts** file with the following in your functions folder:

```typescript
export const config = {
  firebase: {
    ... firbase key information
  },
  uri: 'YOUR DGRAPH GRAPHQL URL',
  admin_email: 'YOUR EMAIL'
};
```

Edit **index.ts** to this:

```typescript
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import firebase from "firebase/app";
import "firebase/auth";
import fetch from 'node-fetch';
import { config } from "./config";

admin.initializeApp();
firebase.initializeApp(config.firebase);

exports.addUser = functions.auth
  .user()
  .onCreate(async (user: admin.auth.UserRecord) => {
    const claims = {
      "https://dgraph.io/jwt/claims": {
        "EMAIL": user.email,
        "ROLE": user.email === config.admin_email ? 'ADMIN' : 'USER'
      }
    };
    return admin
      .auth()
      .setCustomUserClaims(user.uid, claims)
      // create user in dgraph
      .then(async () => {

        // get firebase token
        const token: any = await admin.auth().createCustomToken(user.uid, claims)
          .then((customToken: string) =>
            // use temp custom token to get firebase token
            firebase.auth().signInWithCustomToken(customToken)
              .then((cred: firebase.auth.UserCredential) => cred.user?.getIdToken()))
          .catch((e: string) => console.error(e));

        // add the user to dgraph
        return await fetch('http://' + config.uri, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-Auth-Token': token
          },
          body: JSON.stringify({
            query: `mutation addUser($user: AddUserInput!) {
              addUser(input: [$user]) {
                user {
                  email
                  displayName
                  createdAt
                }
              }
            }`,
            variables: {
              user: {
                email: user.email,
                displayName: user.displayName,
                createdAt: new Date().toISOString()
              }
            }
          })
        })
          .then((r) => r.json())
          .then((r) => console.log(JSON.stringify(r)))
          .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. This will guarantee data consistency in your database.

Remember to edit the mutation depending on your user schema.

Note: You have to import `firebase` as well as `firebase-admin` in order to load the user data from the custom token to a firebase token. You could technically do this by the REST api if you don't want to load `firebase`.

**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](https://github.com/jdgamble555/angular-fire-dgraph/blob/master/src/app/core/auth.service.ts) or [This One](https://github.com/jdgamble555/sapper-urql-ssr/blob/master/src/modules/firebase.ts)

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

https://fireship.io/lessons/angularfire-google-oauth/

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 don't have to refresh the token at that point, as the session will automatically do this.
- When you first login, the custom claim will not yet be present. My code below simply checks for the custom claim, and automatically refreshes when necessary.

```typescript
  async getToken(): Promise<any> {
    return await new Promise((resolve: any, reject: any) =>
      this.afa.onAuthStateChanged((user: firebase.User | null) => {
        if (user) {
          user?.getIdTokenResult()
            .then(async (r: firebase.auth.IdTokenResult) => {
              const token = (r.claims["https://dgraph.io/jwt/claims"])
                ? r.token
                : await user.getIdToken(true);
              resolve(token);
            }, (e: any) => reject(e));
        }
      })
    );
  }
```
- 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). 

Remember, we need a promise since we are getting this data for graphql.

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](https://github.com/jdgamble555/realtime-todos/blob/master/src/urql.ts).  **URQL** is recommended over **Apollo** as it is faster in all cases, and handles caching better.  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: Custom Claims**

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

```typescript
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...

```typescript
const changeRole = firebase.functions().httpsCallable('changeRole');
changeRole({ role: 'MODERATOR' });
changeRole({ userId: 'sleisllekt', role: 'MODERATOR' });
```
**Step 7 - Security and Data Integrity** - Current and the Future

You need to use the [@auth directive](https://dgraph.io/docs/graphql/authorization/directive/) in order to secure your data.

```
type User @auth(
    add: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
    delete: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
) { 
```
The add rule is evaluated as if the user has been added to the database. So in this case, you can't create an admin, unless the user is an ADMIN.  

DGraph is a child in its ability to do backend validation, but hopefully it will fix these problems later.

\+ 1 for **pre-hooks** and [other backend security options](https://discuss.dgraph.io/t/moving-graphql-authorization-to-admin-api/13304)

For the moment, if you need more validation, the only current way is to use a lambda mutation, or a [post-hook](https://dgraph.io/docs/graphql/lambda/webhook/). 

## Standard Claims Version
___

If you're using standard claims, simply login with [firebase](https://firebase.google.com/docs/reference/js/firebase.User) in your framework, then run:

```typescript
user.getIdToken();
```
That's it!

[Here](https://stackoverflow.com/questions/45563320/how-to-get-the-id-token-from-firebaseauth) are some more examples.

If you want to be 100% sure the user is logged in (there are many ways to get the user object, but some can return the incorrect data in some cases), you could use my complicated code from above minus the custom claims checks:

```typescript
export async function getToken(): Promise<any> {
  return await new Promise((resolve: any, reject: any) =>
    auth.onAuthStateChanged(async (user: firebase.User | null) => {
      if (user) { resolve(await user.getIdToken()); }
    }, (e: any) => reject(e)));
}
```
Remember to attach the token to the every query like from above.

**Add user from client**

You can simply create the user IFF the user is a new user:

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

        }
        return null;
      });
```

Obviously this depends on your framework etc.

Here is an angular example with HttpClient, but you may want to use fetch in other frameworks:

```typescript
// execute dgraph mutation here to create the user
// this depends on your schema
const gql = new Dgraph('user').set({
  email: credential.user?.email,
  displayName: credential.user?.displayName,
  createdAt: new Date().toISOString()
}).add().build();

const data = await this.http.post(
  'https://' + environment.uri,
  JSON.stringify({ query: gql }),
  { headers: { 'Content-Type': 'application/json' } }
).toPromise();
```
Here I am using my [easy-dgraph](https://dev.to/jdgamble555/easy-dgraph-create-dgraph-graphql-on-the-fly-10bm) package to quickly create the user.

**Note:** You probably do not want to use URQL or APOLLO here for this one instance. You do not want to import your module that is dependent on your firebase module etc, causing an infinite loop.

**Schema**

For the moment, you cannot lock the field instead of the whole type.  However, you can create a new type for the role. Now in this case, you need a user and role schema:

You should add your ADMIN user as an ADMIN before you create theses rules, so one user can have access at all times.

**Note:** $email is in the Firebase standard claim.

```typescript
type User {
  role: Role!
  email: String!
  ...
}
type Role {
  name: String!
  users: [User] @hasInverse(field: role)
  ...
}
```
By creating a Role Type, you allow a user to update his or her fields, but can set your own [auth rules](https://dgraph.io/docs/graphql/authorization/directive/) on the Role Type. 

Firebase Demo Apps:

- [Custom Claims](https://github.com/jdgamble555/angular-fire-dgraph)
- [Standard Claims](https://github.com/jdgamble555/sapper-urql-ssr)

There is really not much difference in the apps, what is important is the lack of firebase functions.

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.

Dgraph is still a baby when it comes to @auth rules, but I foresee the future in just the next year bringing simplicity to all of this!

Let me know if I missed something, 

J