Apollo Graphql で Firebase authentication を使った認証つきリクエスト
apollographqlfirebase-authentication
最近、Graphql を使い始めた。まだ手探り状態で理解は浅いが、「Apollo Graphql で認証つきリクエスト」を実装したので、まとめておく。
構成
フロントエンドは React (Next.js) を利用。認証は Firebase authentication を利用。Graphql ライブラリとして、Apollo を採用。
実装内容
1. ApolloProvider
まず、_app.tsx
に ApolloProvider
を設置。
pages/_app.tsx
import { ApolloProvider } from "@apollo/client";
import { apolloClient } from "@src/graphqlClient/apollo";
import "@src/styles/globals.scss";
import type { AppProps } from "next/app";
function MyApp({ Component, pageProps, router }: AppProps) {
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
client
ApolloProvider
に渡す client は別ファイルで管理している(記載量がそこそこあるので)。
設定ファイル内の実装方法について、まずは下記の流れで試してみた。
getIdToken
で、idToken を取得する- idToken をヘッダーにセットする。
これだけだと、初期ロード時・リロード時は auth.currentUser
が null となって、idToken が取得されず、usersQuery
的な認証リクエストを叩いた時にデータが取得できない問題にぶち当たった。
そこで、ログイン時に localStorage に idToken をセットすることにして、「idToken を取得できなかった場合に、localStorage に存在する idToken を利用する」形に変更した。
これで、リロードをしても問題なくデータを取得できるようになった。
src/graphqlClient/apollo.ts
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { config } from "@site.config";
import { getFirebase } from "@src/lib/firebase/client";
const httpLink = createHttpLink({
uri: `${config.siteRoot}/api/graphql`,
});
const authLink = setContext(async (_, { headers }) => {
let idToken: string | null = null;
// 👇 localStorage から idToken を取得(一度ログイン済みであれば、セットされている)
const localIdToken = localStorage.getItem("idToken");
const { auth, getIdToken } = await getFirebase(); // 👈 firebase を動的 import しているだけ
if (auth && auth?.currentUser) {
try {
idToken = await getIdToken(auth.currentUser, true);
localStorage.setItem("idToken", idToken);
} catch {
idToken = null;
}
}
// 👇 idToken が取得できなければ、localStorage から取得した idToken を利用する
if (!idToken) {
idToken = localIdToken;
}
// 👇 header にセット
return {
headers: {
...headers,
authorization: idToken ? `Bearer ${idToken}` : "",
},
};
});
const _client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
export const apolloClient = _client;
api/graphql
残りはpages/api/graphql
ページで、「idToken を検証して、カスタムトークンを取得する」処理を書くだけ。
Access-Control
などの設定は他サイトを参考にしただけで、理解が浅いので、また今度調べたい。
pages/api/graphql.ts
import admin from "@src/lib/firebase/server"; // 👈 初期化済の admin を import している
import { ApolloServer, gql } from "apollo-server-micro";
import { join } from "path";
import { readFileSync } from "fs";
import { resolvers } from "@src/graphqlServer/resolvers";
import type { NextApiRequest, NextApiResponse } from "next";
// node を実行している環境からのパス
const schemaFilePath = join(process.cwd(), "graphql", "schema.graphql");
const typeDefs = gql(readFileSync(schemaFilePath).toString());
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
// 👇 context: 実行時に全ての resolve function に渡される
context: async ({ req }) => {
const idToken = getIdTokenFromReq(req);
if (idToken) {
// idToken の検証
const decoded = await admin.auth().verifyIdToken(idToken);
const uid = decoded?.uid;
return { idToken, uid };
} else {
return { idToken: null, uid: null };
}
},
});
const startServer = apolloServer.start();
export default async function (req: NextApiRequest, res: NextApiResponse) {
// CORS リクエストには Cookie が自動で付与されないので、Cookie の付与を許可
res.setHeader("Access-Control-Allow-Credentials", "true");
// 許可する Origin の設定
res.setHeader(
"Access-Control-Allow-Origin",
"https://studio.apollographql.com"
);
// Access-Control-Allow-Headers は Access-Control-Request-Headers を含むプリフライトリクエストへのレスポンスで、実際のリクエストの間に使用できる HTTP ヘッダーを指定する
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
if (req.method === "OPTIONS") {
res.end();
return false;
}
await startServer;
await apolloServer.createHandler({
path: "/api/graphql",
})(req, res);
}
const getIdTokenFromReq = (req: NextApiRequest) => {
const idToken = req.headers["authorization"] as string;
return idToken?.replace(/^Bearer (.*)/, "$1");
};
export const config = { api: { bodyParser: false } };
query
こんな感じで、uid を利用している。
resolvers/query
import admin from "@src/lib/firebase/server";
import type {
QueryResolvers,
Post
} from "@src/types/generated/serverGraphql";
export const queryResolvers: QueryResolvers = {
posts: async (parent, args, { uid }) => {
const db = admin.firestore();
const collectionRef = db.collection("users").doc(uid).collection("posts");
const querySnap = await collectionRef.get();
const posts = querySnap.docs.map(doc => doc.data()) as Post[];
return posts;
},
};