🔐

[WIP] Next.js (Auth.js) と Backend API によるメールアドレス認証の実装例

2024/04/02に公開

🎯目的

Next.js (Auth.js) と Backend API 構成によるアプリにおけるメールアドレス認証の実装例をまとめて、再現できるようにする

💡前提

この記事では、 Frontend と Backend API でメールアドレス認証を行う実装例をまとめています。
GitHub, Google などの OAuth 認証の実装例は以下の記事でまとめておりますので、ご参照ください。

https://zenn.dev/taiyou/articles/147e0a63d236d5

また、この例では、Frontend に Next.js を利用し、 Backend API には FastAPI を用いています。Next.js プロジェクトの作成方法は以下の記事に記載しているので、必要な方は以下の記事の手順に沿ってプロジェクトを作成してください。

https://zenn.dev/taiyou/articles/bee72ea562de4f

💡システムアーキテクチャ

💡メールアドレス認証の仕様

🛠️ 実装

🛠️ 1. Auth.js のセットアップ

はじめに、 Auth.js (NextAuth.js V5) をインストールします。

npm install next-auth@beta
src/route.ts
src/route.ts
/**
 * 未認証でもアクセス可能なページパスの一覧。 
 * @type {string[]}
 */
export const publicRoutes = ["/auth/new-verification"];  // メールアドレスの確認画面

/**
 * 認証系で利用するページパスの一覧。
 * ログイン済みの場合は、DEFAULT_LOGIN_REDIRECT に遷移する。
 * @type {string[]}
 */
export const authRoutes = [
    "/auth/login",   // ログイン画面
    "/auth/register",  // 新規登録画面
    "/auth/error",  // 認証エラー画面
    "/auth/reset",  // パスワードリセット画面
    "/auth/new-password",  // パスワード再設定画面
];

/**
 * 認証で用いる API パスのプレフィックス
 * `src/app/api/auth/[...nextauth]/route.ts` へルーティングできるように定義する
 * @type {string}
 */
export const apiAuthPrefix = "/api/auth";

/**
 * ログイン済みのユーザーがデフォルトでリダイレクトするページパス
 * @type {string}
 */
export const DEFAULT_LOGIN_REDIRECT = "/";
src/auth.config.ts
src/auth.config.ts
import type { NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import {apiAuthPrefix, authRoutes, DEFAULT_LOGIN_REDIRECT, publicRoutes} from "@/route";
import {ExtendedUser} from "@/next-auth";

export default {
    providers: [
        Credentials({
            credentials: {
                email: { label: "メールアドレス", type: "email" },
                password: { label: "パスワード", type: "password" },
            },
            async authorize(credentials) {
                const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/token`, {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        email_address: credentials?.email,
                        password: credentials?.password
                    })
                });

                if (res.status !== 200) return null;

                const token = await res.json();
                if (token) {
                    return { email: credentials?.email, accessToken: token.access_token } as ExtendedUser;
                }
                return null;
            },
        }),
    ],
    secret: process.env.AUTH_SECRET,
    pages: {
        signIn: '/auth/login',
        error: '/auth/error'
    },
    callbacks: {
        authorized({ request, auth }) {
            // ログイン / 未ログイン時の画面遷移を制御する

            const { nextUrl } = request;
            const isLoggedIn = !!auth;

            const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix);
            const isPublicRoute = publicRoutes.includes(nextUrl.pathname);
            const isAuthRoute = authRoutes.includes(nextUrl.pathname);

            if (isApiAuthRoute) {
                // /api/auth は未認証でもアクセス可能
                return true;
            }

            if (isAuthRoute) {
                if (isLoggedIn) {
                    // すでにログイン済みの場合は、リダイレクトさせる
                    return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
                }

                // 未ログインで認証ページの場合は、アクセス可能
                return true;
            }

            return !(!isLoggedIn && !isPublicRoute);
        },
        async jwt({ token, user }) {
            return { ...token, ...user };
        },
        async session({ session, token }) {
            session.user = token as any;
            return session;
        },
    }
} satisfies NextAuthConfig;
src/next-auth.d.ts
src/next-auth.d.ts
import NextAuth, { type DefaultSession } from "next-auth";

// 独自のログインユーザーの型を定義
export type ExtendedUser = DefaultSession['user'] & {
  email: string;
  accessToken: string;
};

declare module "next-auth" {
    interface Session {
        user: ExtendedUser;
    }
}
src/auth.ts
src/auth.ts
import NextAuth from "next-auth";
import authConfig from "@/auth.config";

export const {
    handlers: { GET, POST },
    auth,
    signIn,
    signOut,
} = NextAuth({
    ...authConfig,
});
src/app/api/auth/[...nextauth]/route.ts
src/app/api/auth/[...nextauth]/route.ts
export { GET, POST } from "@/auth";

🔗 APPENDIX

🔗 Auth.js

以下の YouTube と GitHub のソースコードで Auth.js の実装を学びました。

https://www.youtube.com/watch?v=wm5gMKuwSYk

https://github.com/hafizn07/next-auth-v5-advanced-guide-2024

ドキュメントとしては、次の資料が参考になりました。

https://nextjs.org/docs/app/building-your-application/authentication

https://nextjs.org/learn/dashboard-app/adding-authentication

次の YouTube では Auth.js × Backend API の認証方法の実装例として参考になりました。
https://www.youtube.com/watch?v=fYObrr3jf0w

🔗 FastAPI の実装を参考にした

https://github.com/fullstackbook/fullstackbook-jwt-fastapi/blob/main/dependencies/auth.py

🔗 Google OAuth

https://github.com/mjunaidca/nextjs-google-auth-fastapi/blob/main/api/index.py

https://github.com/nextauthjs/next-auth/discussions/8884

Discussion