Skip to content

4. NextAuth ログイン制御の実施・機能拡張

公開日

表紙

  1. Next.js 環境構築
  2. PrismaとPostgreSQLの設定
  3. NextAuth ログイン機能の実装
  4. NextAuth ログイン制御の実施・機能拡張 👈ココ
  5. Tiptapでマークダウンエディタ作成
  6. 日記の登録
  7. 画像のアップロード MinIOの設定

概要

ログインが実装できたので、ログイン状態によってダッシュボード画面を開くか、ログイン画面を開くかの制御を行います。

ログイン状態を取得

NextAuth.jsではクライアント側・サーバ側どちらからでも取得できるようになっています。

サーバ処理のほうが高速なので、必要がなければそちらを使います。

ログイン状態の取得には getServerSession を使います。

Next.js | NextAuth.js
getServerSession
Next.js | NextAuth.js favicon https://next-auth.js.org/configuration/nextjs#in-app-router
  • INFO useSessionとは対照的に、useSessionはユーザーがログインしているかどうかに関係なく(クッキーが存在するかどうかにかかわらず)、常にセッションオブジェクトを返します。一方でgetServerSessionは、ユーザーがログインしている場合にのみ(認証されたクッキーが存在する場合のみ)、セッションオブジェクトを返し、それ以外の場合はnullを返します。

ダッシュボード画面にgetServerSessionを追加して、未ログインの場合はログイン画面にリダイレクトします。

src\app(dashboard)\page.tsx
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
 
export default async function Home() {
  const session = await getServerSession(authOptions);
  if (!session) {
    redirect("/login");
  }
  return <div>dashboard</div>;
}

同様にログイン済の場合はログイン画面を表示せず、ダッシュボードにリダイレクトさせます。ただし、ログイン画面はパスワードログインを行うので、こちらはクライアントコンポーネントとします。クライアントサイドで getServerSession が使えないので、useSessionでログイン状態を確認します。

Client API | NextAuth.js
The NextAuth.js client library makes it easy to interact with sessions from React applications.
Client API | NextAuth.js favicon https://next-auth.js.org/getting-started/client#usesession
src\app(auth)\login\page.tsx
"use client";
 
import GoogleSignInForm from "@/components/auth/google-signin-form";
import { SessionProvider, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
 
export default function LoginPage() {
  return (
    <SessionProvider>
      <Login />
    </SessionProvider>
  );
}
 
function Login() {
  const { data: session } = useSession();
  const router = useRouter();
  if (session) {
    router.push("/");
  }
 


useSessionを利用する場合はSessionProviderが必要となるため、合わせて追加しています。

※サーバーコンポーネントでSessionProviderは利用できません。

ログイン済みの場合は useRouter を使ってダッシュボード画面に遷移させます。

ダッシュボードにログインユーザーの情報を表示する

ログイン制御ができたので、セッションから情報を取得していきます。

sessionに含まれる情報を確認しておきます。

src\app(dashboard)\page.tsx
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
 
export default async function Home() {
  const session = await getServerSession(authOptions);
  if (!session) {
    redirect("/login");
  }
  return (
    <div>
      dashboard
      <p>{JSON.stringify(session)}</p>
    </div>
  );
}

画面から確認すると

4-2

json
{"user":
  {
    "name":"Googleのユーザ名",
    "email":"Googleのメールアドレス",
    "image":"アイコン画像URL"
  }
}

がセッションに含まれていることが分かります。

ログインユーザに紐づく日記を作成したいですが、このままではどのユーザでログインしているかの情報が取得できないため、ユーザーIDをセッションからも参照できるように修正します。

セッションからユーザーIDが取得できるようにする

セッションに自分が欲しい値を追加するには、authOptionsにcallbacksに処理を追加します。

Callbacks | NextAuth.js
Callbacks are asynchronous functions you can use to control what happens when an action is performed.
Callbacks | NextAuth.js favicon https://next-auth.js.org/configuration/callbacks

説明にある通りですが、以下のような振る舞いをします。

  • JWTのコールバック関数はセッションが作成されるとき(≒セッションにアクセスされるとき)に必ず呼び出されます。
  • 引数のuser、account、profileおよびisNewUserは、ユーザーがサインインした後、新しいセッションでこのコールバックが初めて呼び出されるときにのみ渡されます。それ以降の呼び出しでは、tokenのみが使用可能になります

そのため、ログイン時にuserからidを取得し、jwtトークンに暗号化して含める……という実装を行います。

Callbacks | NextAuth.js
Callbacks are asynchronous functions you can use to control what happens when an action is performed.
Callbacks | NextAuth.js favicon https://next-auth.js.org/configuration/callbacks#jwt-callback
src/lib/auth.ts
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import db from "./db";
 
export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(db),
  secret: process.env.NEXTAUTH_SECRET, //JWTの暗号化に使う
  session: {
    strategy: "jwt", //デフォルトでjwt。今回は明示的に指定
  },
  pages: {
    signIn: "/login", //ログインページのURL
  },
  debug: false, //デバッグtrueにすると大量のログが出力されるので注意
  providers: [
    //https://developers.google.com/identity/protocols/oauth2?hl=ja
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    //https://next-auth.js.org/configuration/callbacks
    async jwt({ token, user }) {
      //authorize(認証時)のみuserに値が入ってくるので、tokenに追加しておく(sessionで使うため)
      if (user) {
        return {
          ...token,
          // name: user.name,
          id: user.id,
        };
      }
      console.log("jwt", token);
      return token;
    },
  },
};

上記修正を行った状態で改めてログインし直し、ログを確認してみます。

※公式のサンプルではprofileからidを取得する手順となっていますが、存在しないと怒られます。

※ログイン状態が残っている場合は、Cookieを削除するなどしてログアウトして実施してください。

jwt
jwt {
  name: '',
  email: '',
  picture: '',
  sub: '',
  id: '',
  iat: 1705057977,
  exp: 1707649977,
  jti: ''
}

tokenにidが追加されていることが分かります。sub(jwtのユーザー識別子)がidが同値であることが分かりますが、システムで利用したいidはidとして持たせます(※ここのベストプラクティスは分かっていません)。

sessionにユーザーIDを追加する

JWTには追加されていますが、画面に出力しているsessionにはまだIDが追加されていません。

sessionのコールバックも追加して、ユーザーIDを追加します。

Callbacks | NextAuth.js
Callbacks are asynchronous functions you can use to control what happens when an action is performed.
Callbacks | NextAuth.js favicon https://next-auth.js.org/configuration/callbacks#session-callback
tsx:src/lib/auth.ts
callbacks: {
    //https://next-auth.js.org/configuration/callbacks
    async jwt({ token, user }) {
 
    },
    async session({ session, token }) {
      session.user.id = token.id;
      return session;
    },
  },

DefaultSession型にidがないため、以下でエラーが表示されます。

session.user.id = token.id;

idを保持するSessionの型定義も合わせて追加します。

TypeScript | NextAuth.js
NextAuth.js has its own type definitions to use in your TypeScript projects safely. Even if you don't use TypeScript, IDEs like VSCode will pick this up to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes links to documentation, examples, and other valuable resources.
TypeScript | NextAuth.js favicon https://next-auth.js.org/getting-started/typescript#extend-default-interface-properties
src/types/next-auth.d.ts
//https://next-auth.js.org/getting-started/typescript
//記載されている定義を参考に、src/types/next-auth.d.tsを作成
import "next-auth";
 
declare module "next-auth" {
  /**
   * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
   */
  // Userの定義を追加する
  interface User {
    name: string;
    id: string;
  }
  interface Session {
    user: User & DefaultSession["user"];
  }
}

再度画面を確認すると、セッションにユーザーIDが格納されていることが確認できます。

4-3

ヘッダにアバターを追加

shadcn/ui のアバターコンポーネントを利用して実装していきます。

Avatar
An image element with a fallback for representing the user.
Avatar favicon https://ui.shadcn.com/docs/components/avatar
Avatar
terminal
$ pnpm dlx shadcn-ui@latest add avatar

AvatarImageにuser.imageを設定し、AvatarFallbackには名字2文字を設定します。AvatarFallbackはAvatarImageの取得に失敗した場合に表示されるアイコン(イニシャル等)です。

src/components/user-avatar.tsx
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
 
async function UserAvatar() {
  const session = await getServerSession(authOptions);
  if (!session) {
    redirect("/login");
  }
 
  return (
    <Avatar>
      <AvatarImage src={session.user?.image!} />
      <AvatarFallback>
        {session.user?.name?.slice(0, 2).toUpperCase()}
      </AvatarFallback>
    </Avatar>
  );
}
 
export default UserAvatar;

Tips

DiceBearを使うとアバターアイコンが簡単に作成・利用できます。

DiceBear | Open Source Avatar Library

APIに対してSeed値を渡すとランダムの画像を返してくれるので、モック用アカウントなどでも有用です。

api.dicebear.com
api.dicebear.com favicon https://api.dicebear.com/7.x/open-peeps/svg?seed=test

headerに適用します。モバイル端末では非表示としておきます。

src/components/header.tsx
import { ChevronDown } from "lucide-react";
import Link from "next/link";
import UserAvatar from "./user-avatar";
 
async function Header() {
  return (
    <header className="z-50 flex w-full flex-wrap border-b border-gray-200 bg-white py-3 text-sm sm:flex-nowrap sm:justify-start sm:py-0 dark:border-gray-700 dark:bg-gray-800">
      <nav
        className="relative mx-auto w-full max-w-7xl px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8"
        aria-label="Global"
      >
        <div className="flex items-center justify-between">{/** 省略 **/}</div>
        <div
          id="navbar-collapse-with-animation"
          className="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 sm:block"
        >
          {/** 省略 **/}
        </div>
        <div className="hidden sm:ml-6 sm:block">
          <UserAvatar />
        </div>
      </nav>
    </header>
  );
}
 
export default Header;

4-1