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 取得したほうが良いよね?とか気づきづらいかもしれない。個人的に自分はまだレベルが低いので明示的にコードに書く方が処理の流れの見通しが良くて好きでした
おわり ₍ ᐢ. ◡ .ᐢ ₎
[参考]