Firebase JWT with Flutter

Hi all!

I am new to Dgraph. I switched over from Firebase after I hit some NoSql limitiations. Using Firebase, authentication and authorization was easy, as it is all on the same platform.
Now I want to keep my authentication on Firebase, but I want to move my authorization and rules over to Dgraph. From some research I did, I understand that this would be done through JWTs that Firebase creates and then sends over to Dgraph Cloud through cloud functions.

Never having used cloud functions or JWTs before, I would love to get some guidance as to how I would implement the above mentioned idea in a mobile app using Flutter. I have users that sign up through email/password or Google and I want them to only be able to send any kind of request if they are authenticated.

I used the guide provided at https://dgraph.io/docs/graphql/todo-app-tutorial/todo-firebase-jwt/ to get started, but the differences between React and Flutter are too big for me to fill in the gaps.

If anyone faced similar issues, or has any recommendations for other ways to solve this, I would greatly appreciate it.

On another note: Is there a tutorial on how to use Dgraph for Flutter developers? To my mind, a significant portion of Dgraph users come from the Flutter world and would benefit from more guidance on using GraphQL and Dgraph in Flutter.

Thank you for your help!
Alex.

3 Likes

Hey,

First off I’d say go through this post(s) for understanding how to implement auth related to cloud → Putting it All Together - Dgraph Authentication, Authorization, and Granular Access Control (PART 1) - Dgraph Blog

I’ve mostly used Android, so going from my experience with that, you can make direct request to your Cloud URI (with headers+cookies) for the required response. Of course that will require you to write some boilerplate, but there isn’t any mobile library for Dgraph Cloud as of now

Fireship.io has some really good videos on this:

I highly recommend his Flutter course, as it makes things very easy:

Now as far as DGraph, it is the same premise as any Framework. You need to pass the token in the header to the graphql endpoint. You cannot use urql on Flutter (as it is a Typescript library), but there is an Apollo package in Flutter for this:

And you just grab the token using the firebase getIdToken(), like in any package. I will be updating my Firebase article in the next few days to reference some new things I have learned since 21.03.X.

J

Hi @jdgamble555 !

Thank you for your help! Funny enough, I think I was actually discussing Firebase limitations with you on the Fireship slack!

As of right now, I did a couple of things, but I could not get this working.

I added

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

to my schema.

I also installed the GraphQL flutter package and set it up. I can access the db and perform all requests just fine using the link consisting of my endpoint and the token. Does this mean the token concatenating process was successful?

static AuthLink authLink = AuthLink(
    getToken: ()  => 'Bearer ${UserService.token}',
  );
  
 static Link link = authLink.concat(_httpLink);

As for the cloud functions, I set up this in my index.js and deployed it to Firebase.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.addUserClaim = functions.https.onCall((data, context) => {
	return admin.auth().getUserByEmail(data.email).then(user=>{
		return admin.auth().setCustomUserClaims(user.uid, {
			"https://dgraph.io/jwt/claims":{
				"USER": data.email
			}
		});
	}).then(() => {
		return {
			message: `Success!`
		}
	}).catch(err => {
		 return err
	})
})

I added the function call to my login method, so that whenever somebody logs in, a claim is added and the token I get, gets added to the client link. In the future, I feel like I should probably re-do this and call the cloud function from an onAuthStateChanged() function, but for testing, this seems fine.

So far so good, but as soon as I add rules to my schema, it seems like I never meet the requirements. I have a User schema, with an email. This is the same email that gets added to the claim I set up.

In my app, I call a function to query a user by their username. The goal is to only allow this if the email attached to that user is the same as the email of the currently logged in user (this is not a real usecase, just my testing example).

query MyQuery($eq: String = "") {
        queryUser(filter: {username: {eq: $eq}}) {
          username
        }
      }

My User schema looks like this:

type User 
@auth(query: { rule: """
       	query($USER: String!) {
          queryUser(filter: {email: {eq: $USER}}) {
            email
          }
				}"""
    	})
    {
  id: ID!
  firebaseId: String 
  email: String @search(by: [hash, fulltext])
  username: String @search(by: [hash, fulltext])
  following: [User] @hasInverse(field: "followers")
  followers: [User]
  profilePhotoUrl: String
  accountCreated: DateTime
  description: String
}

What am I missing here?
Some ideas:

  • The rule is flawed
  • $USER does not fetch the currently logged in user
  • My query doesn’t add up with the rule
  • The user claim is not added properly

Best,
Alex.

Welcome to dgraph…

You should not be able to specifically query your user database, as that is what rule you have. This SHOULD NOT work. If this does, it would be very interesting considering you do not have your headers implemented correctly.

I am NOT a flutter developer, but your header should not use Bearer, this is a hazura thing. Your header should be:

headers: {
  'X-Auth-Token': token
}

Here are a few examples I found:

And a typescript version maybe you can translate:

You also don’t need a callable function. You can just create the custom claim with a user onCreate function:

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

admin.initializeApp();

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

It will not be immediately avaiable on the front end when you create a user unless you refresh the claim (or re-login). You can do this in typescript like so:

  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));
        }
      })
    );
  }

– Which basically says refresh the token manually only if the custom claim does not exist.

However, all this is extraneous as of 21.03.X since the email is already in the standard claim.

Hopefully, this will be on slash dgraph today so you don’t have to fool with firebase functions at all, @hardik?

Hopefully not too much longer regardless…

I hope this helps. I am just not a flutter developer. I can tell you that getToken() should be an async function which gets the token directly from firebase, not from localStorage like you find online in hazura examples. Firebase tokens are already stored in the localStorage and automatically update only when necessary.

J

Thank you. I changed

 static HttpLink _httpLink =
      HttpLink('https://bitter-tree.eu-west-1.aws.cloud.dgraph.io/graphql');

static AuthLink authLink = AuthLink(
    getToken: ()  => 'Bearer ${UserService.token}',
  );
  
 static Link link = authLink.concat(_httpLink);

to

 static HttpLink _httpLink =
      HttpLink('https://bitter-tree.eu-west-1.aws.cloud.dgraph.io/graphql', defaultHeaders: {'X-Auth-Token': UserService.token!});

and just used that link for my endpoint.

I fetch the token as soon as a user logs in, through an async function like you suggested. But I’m still adding the claim via a callable function.
To update the token, a simple re-login does not do the trick for me. I need to restart my app, then it works, but this probably has to do with the way that I set my token through static access and not with a setter. I will change this in the future.

I wanted to understand general token handling and authorization, so thank you for helping me!

As for the onCall() vs onCreate(), you’re saying that it is enough to add that claim once with an onCreate() function? Doesn’t the token change? Or does the token not have anything to do with the claims?

Looking forward to the update!

Best,
Alex.

Yes it has to do with that. Even with a callable function, my code above should detect the claim not existing and automatically update it on the next request. No need to restart the app. Put it in an async function when you can to solve that.

The token expires after 1 hour, and gets automatically updated in the back ground (including in your localStorage without the need to put it there again). The custom claim is there forever.

“Once the custom claims are set, they propagate to all existing and future sessions.” - here

J