Struggling to get Firebase to work

I have followed all of the Dgraph Auth tutorials I can find, and I still cannot get Firebase auth to work. At this point I am able to create a user in Firebase and retrieve the JWT in the app, but the token does not include a namespace and therefore I cannot connect it to my queries/schema.

Note: It looks like Firebase upgraded to v9, and that may mean Dgraph’s firebase docs are out of date.

From what I have read, it sounds like I have to set up a Cloud Function in order to add the Namespace field. I do that here (and I see it running in the Firebase console):

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

exports.addUserClaim = functions.https.onCall(async (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! ${data.email}`,
      };
    })
    .catch((err: any) => {
      return err;
    });
});

However, the JWT I get back isn’t any different than it was before I created the function:

{
  "name": "Jake",
  "picture": "https://lh3.googleusercontent.com/a-/AOh14Gh8FTRv1Ze7DkDH0luxxxkeKTX5cJBCbtuN-=s96-c",
  "iss": "https://securetoken.google.com/contributor-credits",
  "aud": "contributor-credits",
  "auth_time": 1643219275,
  "user_id": "ZD0s2fmOXuQcqIVZaUqQ3Pf8X3e2",
  "sub": "ZD0s2fmOXuQcqIVZaUqQ3Pf8X3e2",
  "iat": 1643223533,
  "exp": 1643227133,
  "email": "jake@xxxx.io",
  "email_verified": true,
  "firebase": {
    "identities": {
      "google.com": [
        "111980424334d4207177753"
      ],
      "email": [
        "jake@xxxxx.io"
      ]
    },
    "sign_in_provider": "google.com"
  }
}

Here is how I retrieve the token:

const [user, loading, error] = useAuthState(fireApp.auth());

  const authLink = setContext(async (_, { headers }) => {
    if (!user) {
      return headers;
    }
    const token = user.getIdToken();
    const key = process.env.NEXT_PUBLIC_NETLIFY_CLIENT_CC;
    return {
      headers: {
        ...headers,
        'X-Auth-Token': token ? token : '',
        // 'DG-Auth': key ?? undefined,
      },
    };
  });

The question is how I add the Namespace and associated fields to the JWT. Once I have that, I believe I can get authorization working.

Update: I was able to adjust the code given in the Dgraph Firebase tutorial to accommodate v9, and I can see that I am calling the Firebase function on the frontend, but Firebase still refuses to return a token with a Namespace defined.

Giving up on Firebase.

You don’t need to create a firebase function for a simple logic. Look at the Standard Claims Version here

J

Hi J. Thank you for responding.

I actually tried that tutorial, but just used uid instead of email for the identifier. It didn’t work (I also tried it using user_id because that what it is called in the token).

I will need Custom Claims anyway (I think), because my goal is actually to have users authenticate via Eth wallet. I was thinking it might be best to try and run a script in Dgraph Lambda to get the JWT. I have to check the signed message from the wallet in the backend anyway, so I was thinking I might be able to do those together.

-Jake

Actually, this is an opportunity to ask a more specific question I have: what is actually happening when the query is made? does Dgraph check for a specific field in the token to see if it matches the requirement set in the rule? If so, how does it identify that field? Is there a list somewhere of the fields it is “compatible” with?

Also, when you try to authenticate with a Standard Claim, does it just ignore the Namespace parameter?

The namespace should be https://dgraph.io/jwt/claims.

Make sure you have:

# 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"]

Also, try logging out and then back in. You need to manually refresh your token once the firebase function runs, however, you can use onTokenChange (or something similar) to look for this.

So, you can’t really include the firebase package in a DGraph lambda. What you can do, however, is fetch the firebase rest api endpoint to get the token you want to generate your claim.

No. It will first look under the key https://dgraph.io/jwt/claims (custom claim). If that does not exists, it will then look as a standard claim {} as a main key. This was added as a feature since firebase by default already has your email as a standard claim.

J

1 Like

Wow this is helpful. Thank you for taking the time to write it. I will work through it and let you know if I succeed.

1 Like

Well I got it working (almost). At some point I will have to write up how. Lots of little traps in Firebase Functions.

But now I have what I hope is the final riddle. In the code below console.log('outside', token) correctly prints to token in the console. console.log('inside', token) registers as undefined. If I take the token string and hard code it into the header, auth works correctly. The issue is only that token is somehow undefined inside the authLink function. Why is it undefined?

 const createApolloClient = () => {
    console.log('outside', token);
    const authLink = setContext((_, { headers }) => {
      console.log('inside', token);
      return {
        headers: {
          ...headers,
          'X-Auth-Token': token,
        },
      };
    });
    return new ApolloClient({
      link: authLink.concat(httpLink),
      // link: httpLink,
      // link: process.env.NODE_ENV === 'production' ? authLink.concat(httpLink) : httpLink,
      cache: new InMemoryCache(),
      ssrMode: typeof window === 'undefined',
    });
  };

  const apolloClient = useApollo(pageProps.initialApolloState, createApolloClient());

The plot thickens. In the code below. inside2 correctly prints to the console!!:

  const createApolloClient = () => {
    console.log('outside', token);
    const candy = 'yum';
    const authLink = setContext((_, { headers }) => {
      console.log('inside', token);
      console.log('inside2', candy);

It’s been a while since I messed with apollo. You do not need to save the firebase token to localstorage, as it is already saved there. This is how Firebase works internally. You just need to be able to use a promise to get it from the local storage inside the apollo setup.

See this code:

https://github.com/jdgamble555/realtime-todos/blob/master/src/apollo.ts

Although I admit this could even be cleaned up a bit.

You might also be interested in my j-dgraph package, which does all this automatically:

J

1 Like

Ok so I finally got token retrieval to work. I am going to post my code here incase anyone else stumbles into this problem. (basically the issue was that Apollo was creating the headers before the token arrived. The key part seems to be that the setContext const has to be asynchronous, but you cannot just make it an async function and stick an await in there. You have to use .then() to get the token out of the getToken() which is a promise.

const httpLink = createHttpLink({
    uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
    credentials: 'same-origin',
  });

  const asyncMiddleware = setContext((_, { headers }) =>
    getToken().then((token) => ({
      ...headers,
      'X-Auth-Token': token,
    }))
  );

  const createApolloClient = new ApolloClient({
    link: asyncMiddleware.concat(httpLink),
    cache: new InMemoryCache(),
    ssrMode: typeof window === 'undefined',
  });

But I have arrived at another issue. I now have the token, or at least the console suggests I do, but its not giving me access to my schema per my auth rules.

First question: is there a good way to see exactly what is being passed with the headers so I can confirm the token is there?

Second question: Do you see anything wrong with this?

Token:

{
  "https://dgraph.io/jwt/claims": {
    "ADDRESS": "0x531518985601Fxxxxxx",
    "UUID": "9e266d5d-7214-41b8-bf0e-a6a2d0cf382f"
  },
  "iss": "https://securetoken.google.com/cc",
  "aud": "contributor-credits",
  "auth_time": 1643911729,
  "user_id": "0x531518985601Fxxxxxxx",
  "sub": "0x531518985601Fxxxxxxx",
  "iat": 1644083283,
  "exp": 1644086883,
  "firebase": {
    "identities": {},
    "sign_in_provider": "custom"
  }
}

Query:

export const ADD_USER_EMAIL = gql`
  mutation AddUserEmail($userId: ID!, $address: String!, $name: String, $description: String, $public: Boolean) {
    addEmailAddress(
      input: { address: $address, user: { id: $userId }, name: $name, description: $description, public: $public }
    ) {
      emailAddress {
        address
        user {
          id
          emailAddresses {
            address
          }
        }
      }
    }
  }
`;

Schema:

type EmailAddress @auth (
  add: { rule:"""
	query($UUID: String!) {
		queryEmailAddress { 
			user(filter: { uuid: { eq: $UUID }}) {
					__typename
					id
				}
			}
	    }"""} 
) {
	address: String! @id
	name: String
	description: String
	public: Boolean @search
	user: User! @hasInverse(field:emailAddresses) 
}

type User {
	id: ID! 
	uuid: String! @id
	emailAddresses: [EmailAddress!] @hasInverse(field:user)
	displayName: String @search(by: [term])
	fullName: String
	creationDate: DateTime
}

# 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":["contributor-credits"]}

Open your site in chrome and go to developer tools network tab and then refresh your page and find the GraphQL request and look at the headers.

Nothing looks wrong at first glance.

2 Likes

Turns out the problem is that I still don’t have the token in the headers. I think it has something to do with NextJS.

If I call this, for example:

  const [token, setToken] = useState<string | undefined>(undefined);

  const getToken = async (): Promise<any> => {
    auth.onAuthStateChanged(async (user) => {
      setLoading(false);
      if (user) {
        const isToken = await user.getIdToken();
        setToken(isToken);
        console.log(isToken);
      } else {
        const isToken = await CustomTokenService();
        setToken(await CustomTokenService());
        console.log(isToken);
      }
    });
  };

console.log(token)

I see the token in the console when I log isToken (inside the getToken function), but get undefined when I log token (from the state outside the function).

Similarly, if I try getting the user with Firebase Hooks:

const [user, loading, error] = useAuthState(auth)
console.log(user)

I get “undefined” even though there is definitely an active user

Let me see if I can get you a working example…

Give me the day.

J

1 Like

So I got it to work by adding the following between when it retrieves the user and when it builds the headers

if (loading) {
    return <><\>
}

Loading is a ‘useState’ with the default ‘true’. I set it to false after the token comes in.

This is pretty hackish and probably a bad way to do it.

Part of the issue is that im having users log in via an Ethereum wallet, the tools for which are all based on hooks, so it all needs to happen within a functional component.

Okay, I was in the middle or writing this hook, may still help:

// user hook with token (use global auth constant)
export function useUser() {
    const _auth = auth;
    const [user, setUser] = useState<User | null>(null);
    const [token, setToken] = useState<string | null>(null);
    useEffect(() => {
        const unsubscribe = onIdTokenChanged(_auth, (iuser) => {
            setUser(iuser);
            iuser
                ? getIdToken(iuser).then((t: string) => setToken(t))
                : setToken(null);
        });
        return () => {
            unsubscribe();
        };
    }, [_auth]);
    return { user, token };
}

You really should be using onIdTokenChanged instead of onAuthStateChanged as it detects a token change as well as login state change.

J

EDIT:

Here is a quick way to get the Token by waiting… you have to set apollo to accept async functions like in my example above:

export const getToken = async () => new Promise<User>((res: any, rej: any) => 
onIdTokenChanged(auth, res, rej))
    .then(async (user: User) => user ? await getIdToken(user) : null);

EDIT 2:

Ok, so I created the most basic working version with NextJS:

I put both URQL and Apollo versions in here, so you can choose which one you want to use.

The lib/urql.ts and lib/apollo.ts work out of the box for websockets and async headers, hence whey they look scary. That being said, I wrote them to be extremely easy to use for all types of setups.

Uncomment the imports in pages/_app.tsx and pages/index.tsx to switch between apollo and urql. This example just prints to the console, so nothing happens in the UI, and you will have to configure your own app with your own schema. That being said, you can get the gist of how it works:

I also had an .env file with NEXT_PUBLIC_ as the prefix as recommended by NextJS.

Let me know if this help,

J

Wow, you are a really helpful guy @jdgamble555. I will try these today. Thank you.