Jump to content

NextAuth.js

Next.js アプリケーションが認証システムを必要とするとき、NextAuth.js は、自分で構築する手間をかけずに、複雑なセキュリティを導入できる優れたソリューションです。NextAuth.js には OAuth 認証をすばやく追加するための豊富なプロバイダーの一覧が付属しており、多くのデータベースや ORM のためのアダプターを提供しています。

コンテクストプロバイダー

アプリケーションのエントリーポイントでは、アプリケーションが SessionProviderでラップされていることがわかります:

pages/_app.tsx
<SessionProvider session={session}>
  <Component {...pageProps} />
</SessionProvider>

このコンテキストプロバイダーによって、アプリケーションはセッションデータを props として渡すことなく、アプリケーションのどこからでもアクセスできるようになります:

pages/users/[id].tsx
import { useSession } from "next-auth/react";

const User = () => {
  const { data: session } = useSession();

  if (!session) {
    // Handle unauthenticated state, e.g. render a SignIn component
    return <SignIn />;
  }

  return <p>Welcome {session.user.name}!</p>;
};

サーバーサイドでセッションを取得する

時には、サーバー上でセッションを要求したいこともあるかもしれません。そのためには、create-t3-appが提供するヘルパー関数 getServerAuthSession を使ってセッションをプリフェッチし、getServerSideProps を使ってクライアントに渡します:

pages/users/[id].tsx
import { getServerAuthSession } from "../server/auth";
import { type GetServerSideProps } from "next";

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const session = await getServerAuthSession(ctx);
  return {
    props: { session },
  };
};

const User = () => {
  const { data: session } = useSession();
  // NOTE: `session` wont have a loading state since it's already prefetched on the server

  ...
}

セッションに user.id を含める

Create T3 App は、NextAuth.js の設定にあるsession callbackを利用して、ユーザー ID をsessionオブジェクトに含めるように設定します。

server/auth.ts
callbacks: {
    session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
      }
      return session;
    },
  },

これは、sessionオブジェクトにアクセスしたときに user.id が型付けされることを確認するために型宣言ファイルと組合せて使用されます。詳細については NextAuth.js のドキュメントにある Module Augmentation を参照ください。

server/auth.ts
import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user?: {
      id: string;
    } & DefaultSession["user"];
  }
}

同じパターンを使って、roleフィールドのような他のデータをsessionオブジェクトに追加することができますが、クライアントに機密データを保存するために使用してはなりません

tRPC との併用

NextAuth.js を tRPC で利用する場合、ミドルウェア を使って、再利用可能で保護されたプロシージャを作成することができます。これにより、認証されたユーザーのみがアクセスできるプロシージャを作成することができます。create-t3-appは、認証されたプロシージャの中でセッションオブジェクトに簡単にアクセスできるように、すべてセットアップしてくれます。

これは、2 段階のプロセスで行われます:

  1. getServerSession 関数を使用して、リクエストヘッダーからセッションを取得します。通常の getSessionの代わりにgetServerSession を使用する利点は、サーバーサイドのみの関数であるため、不要なフェッチ呼び出しが発生しないことです。create-t3-appは、この特殊な API を抽象化するヘルパー関数を作成するので、セッションにアクセスするたびに、NextAuth.js のオプションとgetServerSession 関数の両方をインポートする必要がありません。
server/auth.ts
export const getServerAuthSession = (ctx: {
  req: GetServerSidePropsContext["req"];
  res: GetServerSidePropsContext["res"];
}) => {
  return getServerSession(ctx.req, ctx.res, authOptions);
};

このヘルパー関数を使って、セッションを取得し、tRPC コンテキストに渡すことができます:

server/api/trpc.ts
import { getServerAuthSession } from "../auth";

export const createContext = async (opts: CreateNextContextOptions) => {
  const { req, res } = opts;
  const session = await getServerAuthSession({ req, res });
  return await createContextInner({
    session,
  });
};
  1. ユーザーが認証されているかどうかをチェックする tRPC ミドルウェアを作成します。そして、そのミドルウェアを protectedProcedure で使用します。これらのプロシージャの呼び出し元はすべて認証されていなければなりません。そうでなければ、エラーが投げられるので、クライアントで適切にエラー処理を行えます。
server/api/trpc.ts
export const protectedProcedure = t.procedure.use(({ ctx, next }) =>  {
  if (!ctx.session || !ctx.session.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({
    ctx: {
      // infers the `session` as non-nullable
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
})

セッションオブジェクトは、ユーザーの軽くて最小限の表現であり、いくつかのフィールドしか含んでいません。protectedProceduresを使用するとユーザー id にアクセスでき、データベースからさらにデータを取得するのに使用できます。

server/api/routers/user.ts
const userRouter = router({
  me: protectedProcedure.query(async ({ ctx }) => {
    const user = await prisma.user.findUnique({
      where: {
        id: ctx.session.user.id,
      },
    });
    return user;
  }),
});

Prisma との併用法

NextAuth.js を Prisma と共に動作させるには、多くの初期設定が必要ですが、create-t3-app はこのすべてを処理します。create-t3-app の実行において Prisma と NextAuth.js の両方を選択すると、必要なモデルがすべて設定された、完全に動作する認証システムを手に入れることができます。初期構成として生成されたアプリケーションには、Discord OAuth プロバイダがあらかじめ設定されています。これを使っているのは一番簡単に始められるからで、.envにトークンを設定するだけで準備完了です。しかし、NextAuth.js ドキュメント に従えば、簡単に他のプロバイダを追加することもできます。なお、プロバイダによっては、特定のモデルに余分のフィールドを追加しなければならない場合があります。利用したいプロバイダのドキュメントを読んで、必要なフィールドがすべて揃っていることを確認することをお勧めします。

モデルに新しいフィールドを追加する

UserAccountSessionVerificationTokenのいずれかのモデルに新しいフィールドを追加する場合(ほとんどの場合、変更する必要があるのはUserモデルのみです)、Prisma アダプター が新しいユーザーのサインアップやログイン時に自動的にこれらのモデル上にフィールドを作成することを念頭に置いておく必要があります。したがって、これらのモデルに新しいフィールドを追加する場合は、デフォルト値を指定する必要があります。

例えば、Userモデルに role を追加したい場合、role フィールドにデフォルト値を指定する必要があります。これは User モデルの role フィールドに @default 値を追加することで実現できます:

prisma/schema.prisma
+ enum Role {
+   USER
+   ADMIN
+ }

  model User {
    ...
+   role Role @default(USER)
  }

Next.js ミドルウェアを使った利用法

NextAuth.js を Next.js ミドルウェアで利用する場合、認証に JWT セッション戦略を利用する必要があります。これは、ミドルウェアが JWT である場合にのみ、セッションクッキーにアクセスすることができるためです。デフォルトでは、Create T3 App は、データベースアダプターとして Prisma と組み合わせて、defaultデータベースストラテジーを使用するように構成されています。

デフォルトの DiscordProvider を設定する

  1. Discord Developer Portal の Application セクションに向かい「New Application」をクリックします。
  2. 設定メニューの 「OAuth2 ⇒ General」に行きます
  • Client ID をコピーして、.envDISCORD_CLIENT_IDに貼り付けます。
  • Client Secret の下にある 「Reset Secret」をクリックし、その文字列を.envDISCORD_CLIENT_SECRETにコピーしてください。このシークレット情報は二度と表示されないことと、リセットすると既存のシークレット情報は失効してしまうことについて注意してください。
  • 「Add Redirect」をクリックし、<app url>/api/auth/callback/discord を貼り付ける(ローカル開発サーバの場合の例:http://localhost:3000/api/auth/callback/discord)
  • 変更を保存します
  • 開発用と本番用で同じ Discord Application を使用できますが、推奨はしません。また、開発時にはプロバイダをモックすること検討するのもよいでしょう。

お役立ち情報

リソースリンク
NextAuth.js ドキュメントhttps://next-auth.js.org/
NextAuth.js GitHubhttps://github.com/nextauthjs/next-auth
tRPC キッチンシンク - NextAuth と併用してhttps://kitchen-sink.trpc.io/next-auth