かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React Query, GraphQL Request で Firebase の token を使った GraphQL リクエストをするメモ

firebase Auth の JWT (id token) を header に乗せて GraphQL のクエリを発行するメモ

React-Query + GraphQL request で JWT token を使う方法

環境

  • graphql 16.6.0
  • graphql-request 6.0.0
  • @tanstack/react-query 4.29.3
import { useQuery } from 'react-query';
import { GraphQLClient } from 'graphql-request';

function MyComponent() {
  const client = new GraphQLClient(GRAPHQL_ENDPOINT, { headers: {
    authorization: `Bearer ${token}`,
  } });
  const { data, isLoading, error } = useQuery(
    [QUERY_KWY],
    () => client.request(MyQueryDocument, query_variables)
  );
  // ...
}

Firebase Auth で得られる JWT (id token) の有効期限は 1時間

Firebase ID トークンの有効期間は短く、1 時間で期限切れとなります。
cf. ユーザー セッションの管理  |  Firebase Authentication

Firebase Auth でログイン完了時に getIdToken(auth.currentUser) で JWT が取得できる。ここで取得された JWT を state に保持してクエリ発行時に header に乗せる方法法が考えられるが、JTW token の有効期限が 1時間なのでログイン後 1時間以上経過していると JTW token が expired になってしまう。

上記の問題を解決するには GprahQL のクエリ発行時に新しい JWT token を取得するの良さそう

getIdToken( forceRefresh?: boolean ) : Promise<string>
Returns a JSON Web Token (JWT) used to identify the user to a Firebase service.
Returns the current token if it has not expired. Otherwise, this will refresh the token and return a new one.
cf. User | JavaScript SDK  |  Firebase JavaScript API reference

Frebase を使っている場合は getIdToken() を使えばログインが維持されてれば新しい token が取得できる

Frebase の getIdToken は非同期処理なのでどこで実行させるか

React-Query は hooks なので GraphQL Client が非同期で token を取得するのを待つことができない

🙅 このような書き方はでききない

function MyComponent() {
  const token = await getIdToken(auth.currentUser);
  const client = new GraphQLClient(GRAPHQL_ENDPOINT, { headers: {
    authorization: `Bearer ${token}`,
  } });
  const { data, isLoading, error } = useQuery(
    [QUERY_KWY],
    () => client.request(MyQueryDocument, query_variables)
  );
  // ...
}

🙆 useQuery の第二引数 (Query Function) 内で token を取得するようにすればOK

A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error.
cf. Query Functions | TanStack Query Docs

uswQuery の第二引数 (Query Function) は Promise を返す関数であれば良いので、この中で getIdToken して GraphQLClient を作成してしまえば良い

const buildGraphQlClient = async () => {
  const token = await getIdToken(auth.currentUser);
  return new GraphQLClient(GRAPHQL_ENDPOINT, { headers: {
    authorization: `Bearer ${token}`,
  } });
};

function MyComponent() {
  const { data, isLoading, error } = useQuery(
    [QUERY_KWY],
    async () => {
      const client = await buildGraphQlClient();
      return client.request(MyQueryDocument, query_variables)
    }
  );
  // ...
}

これで 非同期で JWT token を取得し、graphql-request で GraphQLClient を作成して react query を使ってリクエストを発行することができました!

Apollo client はリクエスト発行時に自動的にクライアントを組み立てる仕組みを持っている

The Apollo Link library helps you customize the flow of data between Apollo Client and your GraphQL server. You can define your client's network behavior as a chain of link objects that execute in a sequence
cf. Apollo Link overview - Apollo GraphQL Docs

ざっくり言えば ApolloClient の link に設定している関数が GraphQL のリクエストを発行する際に実行されるというイメージ

import { ApolloProvider , ApolloClient, InMemoryCache, createHttpLink, ApolloLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";

const createApolloClient = () => {
  const httpLink = createHttpLink({
    uri: GRAPHQL_ENDPOINT,
  });
  
  const authLink = setContext(async (_, { headers }) => {
    const user = auth.currentUser;
    headers: {
        ...headers,
        authorization: user ? `Bearer ${await getIdToken(user, true)}` : "",
      },
  });

  return new ApolloClient({
    link: authLink().concat(httpLink),
    cache: new InMemoryCache(),
  });
};

type ApiProviderProps = { children: ReactNode };
export function ApiProvider({ children }: ApiProviderProps): JSX.Element => {
  const client = createApolloClient();
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

client の動的なビルドが機能として提供されているので client を作成して ApolloProvider 保持しておけば使い回せる

所感

GraphQL について経験が浅いので、ライブラリの挙動とかしらなくて非同期で取得できる token をどうやって hooks で使うのか迷ってしまった。
期限付きの JWT を使う機会が多いだろうから Apollo Client についてはクエリを発行する際に client をビルドする機能が提供されていた。一方でシンプルなライブラリである GraphQL Request を使う場合は自分でその部分を実装する必要がある。
Apollo はライブラリに秘匿されているので仕組みを理解してなくてもよしなに動作させることができるが、JWT が expire になるならリクエスト前に新しい token 取得したほうが良いよね?とか気づきづらいかもしれない。個人的に自分はまだレベルが低いので明示的にコードに書く方が処理の流れの見通しが良くて好きでした

おわり ₍ ᐢ. ◡ .ᐢ ₎


[参考]