はじめに
前回まではリクエストがきたセッションに対してログイン済み、未ログインどちらなのかを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による認証を組み込むことも可能ですのでそちらについても次回以降ご紹介していきます。
関連記事
- Next.jsとAuth.jsで認証機能を実装するチュートリアル
2024/09/13 - 10分で完成。AWS Amplify公式テンプレートを使ったNext.jsアプリの簡単デプロイ手順
2024/11/05 - 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 - Next.js と Auth.js を使ったログイン状態に応じたアクセス制御の実装
2024/03/02 - ユーザー向けパスワードリセット機能の実装方法:トークン発行からメール送信、セキュリティ対策まで
2024/04/20