Davis Mariotti
Davis Mariotti

Reputation: 605

Handling firebase initialization delay and id tokens for custom apollo graphql backend

Currently, when I authenticate a user with firebase, I store their auth token in localStorage to be used later to connect to my backend like so:

const httpLink = new HttpLink({uri: 'http://localhost:9000/graphql'})

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization token to the headers
  const token = localStorage.getItem(AUTH_TOKEN) || null
  operation.setContext({
    headers: {
      authorization: token ? `Bearer ${token}` : ''
    }
  })
  return forward(operation)
})

const authAfterware = onError(({networkError}) => {
  if (networkError.statusCode === 401) AuthService.signout()
})

function createApolloClient() {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: authMiddleware.concat(authAfterware).concat(httpLink)
  })
}

My problem with this is that I have no way to refresh the token once it expires. So I tried to use the following to set the authorization token for apollo:

const httpLink = new HttpLink({uri: 'http://localhost:9000/graphql'})

const asyncAuthLink = setContext(
  () => {
    return new Promise((success, reject) => {
      firebase.auth().currentUser.getToken().then(token => {
        success({
          headers: {
            authorization: token ? `Bearer ${token}` : ''
          }
        })
      }).catch(error => {
        reject(error)
      })
    })
  }
)

const authAfterware = onError(({networkError}) => {
  if (networkError.statusCode === 401) AuthService.signout()
})

function createApolloClient() {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: asyncAuthLink.concat(authAfterware.concat(httpLink))
  })
}

This works when the user first authenticates, but once the user refreshes the page, firebase is no longer initialized when my graphql queries are sent to my backend, so the token is not sent with it. Is there a way I can asynchronously wait for firebase.auth().currentUser so this will work? Or is there another approach I should take entirely? As far as I know (100% sure) currentUser.getIdToken only makes a network call if the current token is no longer valid. I think this is acceptable as in cases where the token is not valid, the backend can't respond anyway, so I will need to wait for a token refresh to continue.

Some other ideas I thought of:

Thanks!

Upvotes: 5

Views: 648

Answers (1)

Alex Muñoz
Alex Muñoz

Reputation: 26

I know is a bit late but I was stuck on that as well and found a way to solve it. Maybe is not the best one but at least it works. My approach is to create a Next api endpoint to retrieve the user token using the getUserFromCookies method:

import { NextApiRequest, NextApiResponse } from "next";
import { getUserFromCookies } from "next-firebase-auth";
import initAuth from "../../utils/initAuth";

initAuth();

const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => {
  try {
    const user = await getUserFromCookies({ req, includeToken: true });
    const accessToken = await user.getIdToken();
    return res.status(200).json({ success: true, accessToken });
  } catch (e) {
    console.log(`${e}`);
    return res.status(500).json({ error: `Unexpected error. ${e}` });
  }
};

export default handler;

And then call this endpoint in the apollo client config like that:

import { ApolloClient, InMemoryCache, ApolloLink, HttpLink, concat } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { relayStylePagination } from "@apollo/client/utilities";

const getUserToken = async () => {
  const res = await fetch("http://localhost:3000/api/get-user-token");
  const { accessToken } = await res.json();
  return accessToken;
};

const asyncAuthLink = setContext(async (request) => {
  const token = await getUserToken();
  return { ...request, headers: { authorization: token ? `Bearer ${token}` : "" } };
});

const httpLink = new HttpLink({ uri: process.env.NEXT_PUBLIC_API_URL });

const client = new ApolloClient({
  name: "web",
  version: "1.0",
  uri: process.env.NEXT_PUBLIC_API_URL,
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          users: relayStylePagination(),
        },
      },
    },
  }),
  link: concat(asyncAuthLink, httpLink),
});

export default client;

Upvotes: 0

Related Questions