はじめに
前回まではデータベースとORMツールのセットアップを行なった上でNext.jsのサーバーアクションからデータベースへの登録処理を行いました。
今回はリクエストがきたセッションに対してログイン済み、未ログインどちらなのかを判定しつつ、それぞれの状態でアクセスできる画面の設定を行います。今回の内容は比較的ボリュームのある内容となっているため、実際のログイン処理はこの次の記事でご紹介していきます。
認証システムの設計
非常に小さなシステムではありますが認証の仕組みについて簡単に設計していこうと思います。
メールアドレスとパスワードを使ってログインするというところまでは既にご存知かと思いますが、ログイン済み、未ログインの場合にそれぞれ画面へのアクセスがどこまでできるかという問題です。
最初の図は基本的なユーザーの利用の流れとなります。トップ画面に来たユーザーは、アカウント登録画面でアカウントを登録し、ログイン画面でログイン処理を行いログインできたら設定画面(一般的なウェブサイトでいうマイページの役割。今回はユーザー情報が表示される画面)に遷移します。
(設定画面についてはこれから作成していきます。)
既にアカウントを登録済みの場合は、トップ画面からログイン画面に遷移しログイン処理を行います。
設定画面はユーザー情報が表示される画面となり、未ログイン状態でアクセスされると困るのでアクセス拒否するよう設定します。
またログイン済みのユーザーがアカウント登録画面やログイン画面にアクセスしても特にすることはないため、設定画面にリダイレクトするよう設定します。
このようなイメージでログイン済み、未ログインの場合の画面アクセスを設定して行きます。
ログイン済みユーザーにだけ見せたい画面などがあると思いますが、それをどのように実現するかを解説していくのが今回の記事です。
今回のゴール
この記事の中ではログイン処理は実装しません。まずは画面に対するアクセス制御を設定しつつ、未ログインのユーザーがアクセスできない画面に遷移しようとするとログイン画面に戻されるという処理を実装します。
(ログイン処理についてはこの次の記事で解説して行きます)
Auth.jsについて
Auth.jsは、オープンソースの認証ライブラリです。Next.jsアプリケーションに簡単に認証機能を追加するためのライブラリであり、対応プロバイダーはOAuth (Google, Facebook, Twitterなど)、Email、Credentials、任意のカスタムプロバイダーに対応しています。セッション管理はクッキーを使用したセッション管理が標準で提供され、ユーザーの認証状態を簡単に管理できます。
今回のケースだとブラウザからのリクエストがあったときにそのユーザーがログイン済み、未ログインどちらなのかを判定する役目を担っています。
こちらのページに対応するフレームワークが記載されています。
現在のところNext.js、Astro、Express、Nuxt、Remix、SvelteKitなど様々なフレームワークに対応しております。
またGoogleやGithub、Twitterなどの80以上のサービスと連携できるプロバイダーを提供されているため、これらを使用した認証の仕組みを素早く作ることも可能です。
Next.jsのミドルウェアについて
ミドルウェアをアプリケーションに統合することで、パフォーマンス、セキュリティ、ユーザーエクスペリエンスを大幅に向上させることができます。
ミドルウェアが特に効果を発揮する一般的なシナリオには、次のようなものがあります。
- 認証と認可: 特定のページやAPIルートへのアクセスを許可する前に、ユーザーの身元を確認し、セッションクッキーをチェックする。
- サーバーサイドリダイレクト: 特定の条件(ロケール、ユーザーの役割など)に基づいて、サーバーレベルでユーザーをリダイレクトします。
- パスの書き換え: リクエストプロパティに基づいてAPIルートやページへのパスを動的に書き換えることで、A/Bテスト、機能ロールアウト、レガシーパスをサポートします。
- ボット検知:ボットトラフィックを検知してブロックすることで、リソースを保護します: ボットトラフィックを検出してブロックすることで、リソースを保護します。
- ロギングと分析: ページまたはAPIで処理する前に、リクエストデータを取得して分析します。
今回のケースだとアクセスに来たユーザーがAuth.jsによってログイン済み、未ログインを判定したのちに、どの画面にアクセスしに来たかを見て、適切な画面に振り分ける役目を担っています。
例えば、未ログインのユーザーが設定画面にアクセスしようとしているので、ログイン画面にリダイレクトするというのをミドルウェアで実装して行きます。
Auth.jsのセットアップ
Auth.jsの公式ドキュメントに基本的には合わせて実施して行きます。
Auth.jsライブラリをインストールします。
$ npm install next-auth@beta
次に環境変数AUTH_SECRET
を.env
に登録します。
この環境変数はライブラリがトークンと電子メール検証ハッシュを暗号化するために使用するためのものとなります。
下記のコマンドを実行するとランダムな文字列が返ってきます。
openssl rand -base64 33
このような感じです。
$ openssl rand -base64 33
gw7e/vp3ogJ9/8j3YQVhb+jCjF7aJpaacn20uzqm831i
例えば、上記の文字列が返ってきたら下記のように.env
で定義します。
AUTH_SECRET="gw7e/vp3ogJ9/8j3YQVhb+jCjF7aJpaacn20uzqm831i"
Auth.jsの動作確認
プロジェクトのトップにauth.ts
ファイルを作成します。
import NextAuth from 'next-auth'
import github from 'next-auth/providers/github'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [github],
})
動作確認用としてgithub
認証用のプロバイダーを設定しておきます。
次にNext.jsのAPIルートを設定します。
app/api/auth/[[...nextauth]]
ディレクトリはいかにroute.ts
ファイルを作成します。
import { handlers } from '@/auth'
export const { GET, POST } = handlers
http://localhost:3000/api/auth/providers
にアクセスするとAuth.jsで設定しているプロバイダーの一覧が返ってきます。
Next.jsのミドルウェアの動作確認
プロジェクトのトップにmiddleware.ts
ファイルを作成します。
まずは動作確認用として下記のようなコードを書きます。
import type { NextRequest } from 'next/server'
export function middleware(req: NextRequest) {
console.log('middleware', req.nextUrl.pathname)
return
}
export const config = {
matcher: '/auth/:path*',
}
コードの解説をします。
import type { NextRequest } from 'next/server'
export function middleware(req: NextRequest) {
console.log('middleware', req.nextUrl.pathname)
return
}
ミドルウェアがどのような処理をするかを定義しています。
実際の処理内容としてはリクエストを受け取り、そのリクエストのURLをコンソールログで表示するだけとなります。
export const config = {
matcher: '/auth/:path*',
}
matcher
を使ってどのパスにアクセスがあった際にミドルウェアを動かすかを設定します。
今回は/auth
で始まるパスにアクセスがあった場合にミドルウェアが動きます。
なので2つの処理を合わせると、
`/auth`で始まるパスにアクセスがあった場合に、サーバーサイドでコンソールログにリクエストのURLを表示する
ということになります。
実際にhttp://localhost:3000/auth/register
にアクセスしてみましょう。
下記のようにログが表示されればOKです。
$ npm run dev
> tutorial-nextjs-14-auth@0.1.0 dev
> next dev
▲ Next.js 14.0.4
- Local: http://localhost:3000
- Environments: .env.local, .env
✓ Ready in 1766ms
✓ Compiled in 252ms (232 modules)
✓ Compiled /middleware in 201ms (63 modules)
middleware /auth/register
また、トップ画面http://localhost:3000
にアクセスしても特にサーバーサイドでメッセージが表示されないことも確認できるかと思います。
未ログイン時のアクセス制御の実装
Auth.jsとNext.jsのミドルウェアの動作確認ができましたので実際の実装に入っていきます。
エッジランタイムについて
実装の前にエッジランタイムについて軽く触れておきます。
詳細は下記のドキュメントをご参照ください。
Edge here is borrowed from the network engineering folks and refers to a compute node (i.e. server) that is located on the edge of a network, i.e. closer to the users.
ここでいうエッジとは、ネットワークエンジニアリングの人たちから借りたもので、ネットワークのエッジ、つまりユーザーにより近い場所にあるコンピュートノード(つまりサーバー)のことである。
So when we say edge runtimes, we mean a server-side JavaScript runtime that is not Node.js and is optimized to run on these edge compute nodes (servers). That generally means that the code is executing closer to your users on lower power hardware that is optimized for other things like quick startup times, low memory usage, etc.
つまり、エッジ・ランタイムとは、Node.jsではないサーバーサイドのJavaScriptランタイムを意味し、エッジ・コンピュート・ノード(サーバー)上で実行するように最適化されている。 これは一般的に、起動時間の速さやメモリ使用量の少なさなど、他のことに最適化された低消費電力のハードウェア上で、よりユーザーの近くでコードが実行されることを意味する。
ただしデータベースアクセスについては現状エッジ・ランタイム上で動かすことができないケースが多いため、今回のようにログイン時にデータベースにアクセするような場合を想定するとエッジ・ランタイムでは動かさないように設定していく必要があります。
エッジランタイムで動かす処理とそうでない処理を分割していきます。
プロジェクトトップにauth.config.ts
ファイルを作成します。
このファイルがエッジランタイムで動くものを定義することとなります。
import type { NextAuthConfig } from "next-auth";
import github from "next-auth/providers/github";
export default { providers: [github] } satisfies NextAuthConfig;
次にAuth.jsからPrismaを使うためのライブラリをインストールします。
$ npm install @auth/prisma-adapter
先ほど動作確認で使用したauth.ts
を以下に書き換えます。
+ import { PrismaAdapter } from "@auth/prisma-adapter"
import NextAuth from "next-auth";
+ import authConfig from "./auth.config";
+ import db from "./lib/db";
export const { handlers, signIn, signOut, auth } = NextAuth({
- providers: [github],
+ adapter: PrismaAdapter(db),
+ session: { strategy: "jwt" },
+ ...authConfig,
});
セッション管理をJWTで行うこととし、またGithubプロバイダーをauth.config.ts
で定義しましたのでこちらからは消しておきます。
このように2つのファイルに役割を分割することでNext.jsのミドルウェアでauth.config.ts
を使用できるようになります。
ルートの設定
認証システムの設計のセクションでルートによって異なる挙動となるよう設計しましたが、幾つかの処理で使われるためルートの設定をまとめておきます。
プロジェクトのトップでroute.ts
ファイルを作成します。
/**
* 公開ページのURLを格納する配列
* これらのページについては認証不要でアクセス可能
*/
export const publicRoutes: string[] = ["/"];
/**
* 認証用ページのURLを格納する配列
* これらのページについてはログイン済みの場合、設定ページにリダイレクトさせる
*/
export const authRoutes: string[] = ["/auth/login", "/auth/register"];
export const apiAuthPrefix = "/api/auth";
export const DEFAULT_LOGIN_REDIRECT = "/settings";
ミドルウェアの設定
上記のroute.ts
を用いてNext.jsのミドルウェアを下記のように設定します。
import NextAuth from 'next-auth'
import authConfig from './auth.config'
import {
apiAuthPrefix,
authRoutes,
DEFAULT_LOGIN_REDIRECT,
publicRoutes,
} from './route'
const { auth } = NextAuth(authConfig)
export default auth((req) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix)
const isPublicRoute = publicRoutes.includes(nextUrl.pathname)
const isAuthRoute = authRoutes.includes(nextUrl.pathname)
if (isApiAuthRoute) {
return
}
if (isAuthRoute) {
if (isLoggedIn) {
return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl))
}
return
}
if (!isLoggedIn && !isPublicRoute) {
return Response.redirect(new URL('/auth/login', nextUrl))
}
return
})
export const config = {
matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
}
コードの解説をしていきます。
import authConfig from './auth.config'
(※一部省略)
const { auth } = NextAuth(authConfig)
auth.config.ts
ファイルを呼び出しミドルウェアでAuth.jsが使えるように定義しています。
const { nextUrl } = req
const isLoggedIn = !!req.auth
リクエストからログイン済み、未ログインの判定をしています。Auth.jsを使うとこのように簡単にログイン状態を確認することができます。
const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix)
const isPublicRoute = publicRoutes.includes(nextUrl.pathname)
const isAuthRoute = authRoutes.includes(nextUrl.pathname)
同じくリクエストからどのリソースにアクセスするのかを判定しています。
if (isApiAuthRoute) {
return
}
if (isAuthRoute) {
if (isLoggedIn) {
return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl))
}
return
}
if (!isLoggedIn && !isPublicRoute) {
return Response.redirect(new URL('/auth/login', nextUrl))
}
return
- isApiAuthRoute(
/api/auth
)へのリクエストであれば特に何もしません。 - isAuthRoute(
['/auth/login', '/auth/register']
)へのアクセスで、ログイン済みのセッションであれば、DEFAULT_LOGIN_REDIRECT
(/settings
)へリダイレクトさせます。また未ログインの場合であれば特に何もしません。 - 未ログインでisPublicRoute(
/
)以外のアクセスの場合は、ログイン画面にリダイレクトさせます。
これでアクセス制御の実装は完了しました。
設定画面の準備
最後に動作確認用として設定画面を用意します。
/app/(protected)
配下にlayout.tsx
ファイルを作成します。
const ProtectedLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="flex h-full items-center justify-center bg-gradient-to-br from-sky-100 to-blue-300">
{children}
</div>
)
}
export default ProtectedLayout
次に、/app/(protected)/page
配下にlayout.tsx
ファイルを作成します。
const SettingsPage = () => {
return <div>設定画面</div>
}
export default SettingsPage
動作確認
現在は未ログイン状態なのでhttp://localhost:3000/settings
にアクセスするとログイン画面にリダイレクトされます。
これだけだとイメージつきにくいかと思いますので、ミドルウェアでリダイレクトしている処理をコメントアウトしてどのような挙動になるか確認してみます。
if (isAuthRoute) {
if (isLoggedIn) {
return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl))
}
return
}
- if (!isLoggedIn && !isPublicRoute) {
- return Response.redirect(new URL('/auth/login', nextUrl))
- }
+ // if (!isLoggedIn && !isPublicRoute) {
+ // return Response.redirect(new URL('/auth/login', nextUrl))
+ // }
return
})
再びhttp://localhost:3000/settings
にアクセスし、設定画面が表示されたらOKです。動作確認できましたらミドルウェアのコメントアウトは戻しておいてください。
設定画面がJSONをそのまま表示しており見づらい状態となっていますが後半の章でJSONで返す項目を増やしつつ、見やすいUIに作り替えていく予定です。
さいごに
今回はリクエストがきたセッションに対してログイン済み、未ログインどちらなのかを判定しつつ、それぞれの状態でアクセスできる画面の設定を行いました。
次回は実際のログイン処理を行う画面を作成し、ログイン済みのセッションがアクセス可能な画面にアクセスできることを確認していきます。
次の記事はこちら
関連記事
- Next.jsとAuth.jsで認証機能を実装するチュートリアル
2024/09/13 - Next.jsでログイン画面を作ってメールアドレス/パスワードでログインできるようにする
2024/02/27 - Next.jsでのメール認証処理の実装ガイド:アカウント登録からトークン検証まで
2024/05/10 - Next.jsでのメール認証処理の実装ガイド:トークン検証からログイン画面へのリダイレクト処理までの詳細解説
2024/05/13 - Next.jsを活用したGitHubとGoogleのOAuth認証実装完全ガイド — スムーズなユーザーログインの実現方法
2024/06/11 - Next.jsとmicroCMSで作るブログ:ヘッドレスCMSによるコンテンツ管理と表示
2024/12/16 - ユーザー向けパスワードリセット機能の実装方法:トークン発行からメール送信、セキュリティ対策まで
2024/04/20 - ユーザー向けパスワードリセット機能の実装方法:トークン認証から新しいパスワードの設定まで
2024/04/22