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

やりたいこと
tRPC の認証に firebase auth を利用したい。
イメージとしては、こんな感じを想定している↓
- (client-side) firebase auth でログイン
- (client-side) IdToken を生成し Header に付与した状態で tRPC server に request 送信
- (server-side) Header から idToken を取得し、idToken を検証

Custom header
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 を取得して渡してあげれば良さそう。

これで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}`,
};
},
}),
],
};
},
});

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,
};
},
});

認証前提の 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);
ちゃんと型ついてる

あとは、認証必須の route では protectedProcedure を使ってあげればOK
このスクラップは2023/04/29にクローズされました