はじめに
前回まではリクエストがきたセッションに対してログイン済み、未ログインどちらなのかをAuth.jsの仕組みを活用して判定しつつ、それぞれの状態でアクセスできる画面の設定をNext.jsのミドルウェアを使って行いました。
今回はログイン処理を実装しつつ、ログイン状態でアクセス可能な画面にアクセスできることを確認していきます。
今回のゴール
ログインの入力フォームを作成しログインに成功した場合、設定画面にリダイレクトしセッション情報を表示させます。またログアウトの処理を行うと再びログイン画面に戻ってくる流れとなります。
ログイン時にもチェック機能を実装しパスワードが未入力だった場合や入力したメールアドレス/パスワードが誤っていた場合にユーザーに通知する仕組みも併せて導入します。
メールアドレス/パスワードによるログイン
Auth.jsのCredentials Provider
を使うことで実装が可能となります。
実装の流れ
実装する箇所がこれまでより多岐に渡りコード量もそれなりに多いです。ただ新しいライブラリなどは登場しないため比較的スムーズにコードは理解できるかと思います。
- ①ログイン用のZodスキーマ作成
- ②メールアドレスをキーにデータベースにアクセスする処理の作成
- ③認証処理の作成
- ④ログイン用のサーバーアクションを作成し認証処理の組み込み
- ⑤ログインフォームの作成(バリデーション処理含む)
- ⑥ログイン画面にログインフォームの組み込み
- ⑦設定画面にログアウト処理の作成
①ログイン用のZodスキーマ作成
まず最初にログイン用のZodスキーマを作成していきます。
アカウント作成用のスキーマと似ていますが2点変えています。
- 名前の入力チェックはなし
- パスワードは最低1文字(アカウント作成時にパスワード強度を設定しているため)
/schema
配下のindex.ts
に下記を追記します。
+ export const LoginSchema = z.object({
+ email: z.string().email({
+ message: 'メールアドレスを入力してください',
+ }),
+ password: z.string().min(1, {
+ message: 'パスワードを入力してください',
+ }),
+ })
②メールアドレスをキーにデータベースにアクセスする処理の作成
メールアドレスを元にユーザー情報をデータベースに問い合わせする処理を作成します。
こちらは今後も他の箇所で使用する共通処理とするため、この処理単独で作成します。
/data
配下にuser.ts
ファイルを作成します。
import db from '@/lib/db'
export const getUserByEmail = async (email: string) => {
try {
const user = await db.user.findUnique({ where: { email } })
return user
} catch {
return null
}
}
③認証処理の作成
①と②で作成したスキーマ定義とデータベース問い合わせ処理で使って、認証処理を行います。
+ import bcryptjs from 'bcryptjs'
import type { NextAuthConfig } from 'next-auth'
+ import Credentials from 'next-auth/providers/credentials'
import github from 'next-auth/providers/github'
+ import { getUserByEmail } from './data/user'
+ import { LoginSchema } from './schema'
- export default { providers: [github] } satisfies NextAuthConfig
+ export default {
+ providers: [
+ github,
+ Credentials({
+ async authorize(credentials) {
+ const validatedFields = LoginSchema.safeParse(credentials)
+
+ if (!validatedFields.success) {
+ return null
+ }
+ const { email, password } = validatedFields.data
+ const user = await getUserByEmail(email)
+ if (!user || !user.password) {
+ return null
+ }
+ const passwordMatch = await bcryptjs.compare(password, user.password)
+ if (passwordMatch) {
+ return user
+ }
+ return null
+ },
+ }),
+ ],
+ } satisfies NextAuthConfig
コードの解説をします。
+ Credentials({
+ async authorize(credentials) {
+ const validatedFields = LoginSchema.safeParse(credentials)
+
+ if (!validatedFields.success) {
+ return null
+ }
credentials
の中にはログイン時にユーザーが入力したメールアドレスとパスワードがセットされています。スキーマチェックをして問題があれば、処理を終了します。
+ const { email, password } = validatedFields.data
+ const user = await getUserByEmail(email)
+ if (!user || !user.password) {
+ return null
+ }
ユーザーが入力したメールアドレスを元にデータベースにアクセスしアカウントが登録済みでない、もしくはパスワードが設定されていなければ処理を終了します。
+ const passwordMatch = await bcryptjs.compare(password, user.password)
+ if (passwordMatch) {
+ return user
+ }
ハッシュ化されたパスワードをデータベースから取得したら、ユーザーが入力したパスワードをハッシュ化した上で比較し一致していたらユーザー情報を返し正常終了とします。
④ログイン用のサーバーアクションを作成し認証処理の組み込み
上記で作成した認証処理をサーバーアクションに組み込みます。
/actions
配下にlogin.ts
ファイルを作成します。
'use server'
import { AuthError } from 'next-auth'
import { z } from 'zod'
import { signIn } from '@/auth'
import { DEFAULT_LOGIN_REDIRECT } from '@/route'
import { LoginSchema } from '@/schema'
export const login = async (values: z.infer<typeof LoginSchema>) => {
const validatedFields = LoginSchema.safeParse(values)
if (!validatedFields.success) {
return { error: '入力内容を修正してください' }
}
const { email, password } = validatedFields.data
try {
await signIn('credentials', {
email,
password,
redirectTo: DEFAULT_LOGIN_REDIRECT,
})
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return {
error: 'メールアドレスもしくはパスワードが間違っています。',
}
default:
return { error: 'エラーが発生しました。' }
}
}
throw error
}
}
コードの解説をします。
await signIn('credentials', {
email,
password,
redirectTo: DEFAULT_LOGIN_REDIRECT,
})
signIn
関数を実行すると先ほどのauthorize
が呼び出されます。
処理が正常終了した際のリダイレクト先としてDEFAULT_LOGIN_REDIRECT
(/settings
)を設定しています。
⑤ログインフォームの作成(バリデーション処理含む)
ログイン処理の実装が完了しましたのでログインするための入力フォームを用意しログイン処理を組み込みます。
/component/auth
配下にlogin-form.tsx
ファイルを作成します。
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState, useTransition } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { login } from '@/actions/login'
import { LoginSchema } from '@/schema'
import { FormError } from '../form-error'
import { Button } from '../ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form'
import { Input } from '../ui/input'
import { CardWrapper } from './card-wrapper'
export const LoginForm = () => {
const [error, setError] = useState<string | undefined>('')
const [isPending, setTransition] = useTransition()
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: '',
password: '',
},
})
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
setError('')
setTransition(async () => {
try {
const response = await login(values)
if (response && response.error) {
setError(response.error)
}
} catch (e) {
setError('エラーが発生しました')
}
})
}
return (
<CardWrapper
headerLabel="メールアドレスとパスワードを入力してログイン"
buttonLabel="アカウント作成がまだの方はコチラ"
buttonHref="/auth/register"
showSocial
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>メールアドレス</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="nextjs@example.com"
type="email"
autoComplete="off"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>パスワード</FormLabel>
<FormControl>
<Input
{...field}
placeholder="******"
type="password"
disabled={isPending}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormError message={error} />
<Button type="submit" className="w-full" disabled={isPending}>
ログイン
</Button>
</form>
</Form>
</CardWrapper>
)
}
こちらについてはアカウント作成用の入力フォームと構造はほぼ同じなので、詳しい説明は割愛します。
1箇所異なる点としてはアカウント作成用の入力フォームでは成功した際にメッセージを返していましたが、ログインで成功した場合は設定画面にリダイレクトするため成功した際のメッセージを表示する必要がありません。
そのため、成功時のメッセージを表示するコンポーネントや成功したメッセージを状態管理する処理を削っています。
⑥ログイン画面にログインフォームの組み込み
作成したログインフォームを画面に組み込みます。
import { LoginForm } from '@/components/auth/login-form'
const LoginPage = () => {
return <LoginForm />
}
export default LoginPage
⑦設定画面にログアウト処理の作成
動作確認用にログアウトの処理を設定画面に作成します。
+ import { auth, signOut } from '@/auth'
+ import { Button } from '@/components/ui/button'
- const SettingsPage = () => {
- return <div>設定画面</div>
+ const SettingsPage = async () => {
+ const session = await auth()
+ if (!session) return null
+ const onSubmit = async () => {
+ 'use server'
+ await signOut()
+ }
+ return (
+ <div>
+ 設定画面
+ <pre>{JSON.stringify(session, null, 2)}</pre>
+ <form action={onSubmit}>
+ <Button type="submit" className="w-full" variant="secondary">
+ ログアウト
+ </Button>
+ </form>
+ </div>
+ )
}
export default SettingsPage
コードの解説をします。
+ const session = await auth()
+ if (!session) return null
(※一部省略)
+ <pre>{JSON.stringify(session, null, 2)}</pre>
await auth()
auth.jsはこのコマンドでサーバーサイドでセッションを取得することが可能です。そして取得できたセッション情報を設定画面にて表示します。
+ const onSubmit = async () => {
+ 'use server'
+ await signOut()
+ }
ログアウトする処理となります。
動作確認
一通りコードを実装しましたら、実装した箇所が想定通りの挙動を示すか動作確認をします。
①パスワードが未入力だった場合はバリデーション機能でエラーが返ること
②パスワードが正しくない場合は、ログイン用のサーバーアクションでエラーが返ること
③正しいメールアドレス/パスワードを入力した場合、設定画面にリダイレクトしログインできセッション情報が確認できること
④ログアウトするとログイン画面にリダイレクトすること(Next.jsのミドルウェアの機能)
⑤未ログイン状態で設定画面にアクセスした場合、ログイン画面にリダイレクトすること(Next.jsのミドルウェアの機能)
※④と⑤に関しては前回の記事で実装した機能になります。
さいごに
今回の記事でアカウント作成〜ログイン〜ログアウトまでの一連の処理を終えることできました。
ただ、認証に関しては実施まだ考慮する点がいくつかあります。
- メールアドレス存在有無の確認
- パスワードリセット
- 二要素認証
これらの機能がメールアドレス/パスワードによる認証を行う際には必須となってきます。次回以降はこれらの実装方法に関してもご紹介していきます。
またメールアドレス/パスワードでの認証を実装するのはAuth.jsなどのライブラリを活用してもそれなりに考慮すべき点があり大変かと思います。なるべく認証に関する処理に時間をかけたくないシステムのPoC段階などではGoogleやFacebookなどを活用したOauthによる認証を活用した方がいいかと思います。
Auth.jsでOauthによる認証を組み込むことも可能ですのでそちらについても次回以降ご紹介していきます。
関連する技術ブログ
キャッシュ戦略完全ガイド:CDN・Redis・API最適化でパフォーマンスを最大化
Webアプリの高速化には、適切なキャッシュ戦略が不可欠。本記事では、CDN(Cloudflare / AWS CloudFront)による静的コンテンツ配信、Redis / Memcached を活用したデータベース負荷軽減、APIレスポンスキャッシュ(GraphQL / REST API)など、キャッシュを駆使してパフォーマンスを向上させる方法を解説。TTL設定、Next.js ISR / SSG のプリフェッチ、クエリキャッシュ(React Query / Apollo Client)、キャッシュヒット率の分析など、実践的なノウハウも紹介します。
shinagawa-web.com
チャットアプリ(画像・PDF送信、ビデオ通話機能付き)
お客様固有の要件を除き一般的なチャットアプリに求められる最低限の機能を実装しデモアプリとしてご紹介いたします。
shinagawa-web.com
React × Tailwind CSS × Emotionで実践するコンポーネント設計ガイド:デザインシステム・状態管理・再利用性の最適解
React、Tailwind CSS、Emotion、Storybook、Figma、Next.jsを活用したコンポーネント設計のベストプラクティスを紹介。デザインシステムに基づく命名規則、適切な状態管理、再利用性を高める抽象化、アクセシビリティ対応、スタイルガイドラインの整備、テーマ設定、バージョン管理、ドキュメント作成まで、モダンフロントエンド開発に欠かせない知識を徹底解説します。
shinagawa-web.com
管理ダッシュボード機能(グラフ表示、データ取り込み)
一般的な家計簿アプリとして求められる最低限の機能を実装しデモアプリとしてご紹介いたします。
shinagawa-web.com
Next.jsとAuth.jsで認証機能を実装するチュートリアル
Next.jsでアプリケーションを作る時に必要となる認証機能をどのように実装するかをご紹介する記事となります。アカウント登録から始まり、ログイン、ログアウト、ページごとのアクセス制御、OAuth、二要素認証、パスワードリセットなど認証に関連する様々な機能をコードベースでご紹介します。
shinagawa-web.com
10分で完成。AWS Amplify公式テンプレートを使ったNext.jsアプリの簡単デプロイ手順
AWS Amplifyの公式テンプレートを活用し、Next.jsアプリを素早く効率的にデプロイする方法をわかりやすく解説します。テンプレートの導入からコード修正後の再デプロイまで、初心者にも実践しやすい完全ガイドです。
shinagawa-web.com
Next.js × AWS CDK の統合環境構築:Docker でローカル開発から本番デプロイまで
Next.js と AWS CDK を1つのリポジトリで管理し、Docker を活用してローカル開発環境と本番環境向けのイメージを構築する方法を解説。ディレクトリ構成の設計から、Next.js のセットアップ、Docker Compose による開発環境の構築、ECR 向けの本番用 Docker イメージの作成、CDK の導入までを網羅。
shinagawa-web.com
Next.jsでのメール認証処理の実装ガイド:アカウント登録からトークン検証まで
Next.jsを活用したメール認証の実装方法を解説。アカウント登録時のトークン発行から、Sendgridを使ったメール送信処理まで、具体的な手順を紹介します。
shinagawa-web.com
弊社の技術支援サービス
無駄なコストを削減し、投資対効果を最大化する
クラウド費用の高騰、不要なSaaSの乱立、開発工数の増加――これらの課題に悩んでいませんか?本サービスでは、クラウドコストの最適化、開発効率向上、技術選定の最適化 を通じて、単なるコスト削減ではなく、ROIを最大化する最適解 をご提案します。
shinagawa-web.com
最新技術の導入・検証を支援するPoCサービス
Remix、React Server Components、TypeScript移行、クラウドサービス比較、マイクロサービス、サーバーレス、デザインシステムなど、最新技術のPoC(概念実証)を通じて、最適な技術選定と導入を支援します。貴社の開発課題に合わせた検証・実装で、ビジネスの成長を加速させます。
shinagawa-web.com
開発生産性を最大化するための支援サービス
開発チームの生産性向上、コードの品質管理、インフラの最適化まで、様々な側面からサポートします。コードベースのリファクタリングから、テスト自動化、オンボーディング強化まで、プロジェクトの成功に必要なすべての支援を提供。御社の開発現場が効率的に機能するように、技術的な障害を取り除き、スムーズな開発を実現します。
shinagawa-web.com
開発品質向上支援 – 効率的で安定したプロダクトを実現
フロントエンドからバックエンド、データベースまで、開発プロセス全体を最適化し、安定したプロダクト作りをサポートします。コードレビューの仕組み、型定義の強化、E2Eテスト環境の構築など、開発の各ステップにおけるベストプラクティスを導入することで、より効率的でバグの少ない、そしてユーザー満足度の高いサービス提供を支援します。
shinagawa-web.com
Webアプリのセキュリティ強化支援
Webアプリの脆弱性対策からインフラのセキュリティ強化まで、包括的なセキュリティ支援を提供。OWASP Top 10対策、JWT認証の最適化、APIのアクセス制御、依存パッケージの監査、セキュアコーディングの標準化など、実践的なアプローチで開発現場の安全性を向上させます。
shinagawa-web.com
目次
お問い合わせ