Closed6

Next.js + tRPC に firebase auth を組み込む

nbstshnbstsh

やりたいこと

tRPC の認証に firebase auth を利用したい。
イメージとしては、こんな感じを想定している↓

  • (client-side) firebase auth でログイン
  • (client-side) IdToken を生成し Header に付与した状態で tRPC server に request 送信
  • (server-side) Header から idToken を取得し、idToken を検証
nbstshnbstsh

Custom header

https://trpc.io/docs/client/headers

The headers option can be customized in the config when using the httpBatchLink or the httpLink.

createTRPCNext()config で request 時の header をカスタマイズできるみたい。

// Import the router type from your server file
import type { AppRouter } from '@/server/routers/app';
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';

let token: string;

export function setToken(newToken: string) {
  /**
   * You can also save the token to cookies, and initialize from
   * cookies above.
   */
  token = newToken;
}

export const trpc = createTRPCNext<AppRouter>({
  config(opts) {
    return {
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
          /**
           * Headers will be called on each request.
           */
          headers() {
            return {
              Authorization: token,
            };
          },
        }),
      ],
    };
  },
});

httpBatchLink に設定した header() は request ごとに走るので、ここで idToken を取得して渡してあげれば良さそう。

nbstshnbstsh

これでOK

import { getAuth, getIdToken } from 'firebase/auth';

const getCurrentUserIdToken = async () => {
  const currentUser = getAuth(firebaseApp).currentUser;
  if (!currentUser) return;

  return await getIdToken(currentUser, true);
};

export const trpc = createTRPCNext<AppRouter>({
  config(opts) {
    return {
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
          /**
           * Headers will be called on each request.
           */
          async headers() {
            const idToken = await getCurrentUserIdToken();
            if (!idToken) return {};

            return {
              Authorization: `Bearer ${idToken}`,
            };
          },
        }),
      ],
    };
  },
});          
nbstshnbstsh

Authorization

https://trpc.io/docs/server/authorization

次は server-side で idToken の認証をしていく。
やることとしては、createContext で request header から Authorization Header の idToken を取り出して、firebase-admin auth の verifyIdToken で decode していく。

今回は Next.js なので、trpcNext.createNextApiHandler 内で行う。

export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext: async ({ req }) => {
    const idToken = req.headers.authorization?.replace(/^Bearer /, '');
    if (!idToken) return {};

    const decodedIdToken = await adminAuth.verifyIdToken(idToken);
    if (!decodedIdToken) return {};

    return {
      currentUser: decodedIdToken,
    };
  },
});
nbstshnbstsh

認証前提の Procedure を作成する

create-t3-app のコードを参考に認証必須の protectedProcedure を作る。

先ほど作成した createContext に渡した関数を別の file に移す。(Context の型を利用したい)

context.ts
const validateAuthToken = async (req: CreateNextContextOptions['req']) => {
  const idToken = req.headers.authorization?.replace(/^Bearer /, '');
  if (!idToken) return null;

  const decodedIdToken = await adminAuth.verifyIdToken(idToken);
  if (!decodedIdToken) return null;

  return decodedIdToken;
};

export const createTRPCContext = async ({ req }: CreateNextContextOptions) => {
  const decodedIdToken = await validateAuthToken({ req });

  return {
    currentUser: decodedIdToken,
  };
};

export type Context = Awaited<ReturnType<typeof createTRPCContext>>;

initTRPC.context<Context>() で Context の型を登録し、protectedProcedure を作っていく。

trpc.ts
import { TRPCError, initTRPC } from '@trpc/server';
import { Context } from './conetxt';

const t = initTRPC.context<Context>().create();

// Base router and procedure helpers
export const router = t.router;
export const publicProcedure = t.procedure;

const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.currentUser) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      ...ctx,
      // infers the `currentUser` as non-nullable
      currentUser: ctx.currentUser,
    },
  });
});

export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);

ちゃんと型ついてる

このスクラップは2023/04/29にクローズされました