NextAuthでID/PASSのログイン認証を実装する
公式を読んだ上で不明点を実装しながら検証する。
検証したい要件
- ログイン認証はID/PASS
- ユーザーのGmailと連携し、GmailAPIを叩けるようにする
- 今後SFなど他のサービスとも連携するため、本サービスの認証はID/PASSを利用
- FW:Next.js, ORM:Prismaを利用予定
理解したこと
- NextAuthは、OAuthを推してる。
- ID/PASSログイン認証は、Credentialsプロバイダで実装する。(あんまり推奨していない)
Credentialsプロバイダは、ユーザ名とパスワード、二要素認証、ハードウェアデバイス(YubiKey U2F / FIDOなど)など、任意の認証情報を使ってサインインすることを可能にします。
不明点
- 永続化されないと書いているが、別途DBで管理するようにロジックを挟み込めば問題ないか?
これは、ユーザーを認証する必要がある既存のシステムがある場合の使用をサポートすることを目的としています。
認証情報プロバイダは、セッションで JSON ウェブ トークンが有効になっている場合にのみ使用できます。認証情報プロバイダで認証されたユーザーは、データベースに永続化されません。
- パスワード再設定や新規登録は丸々実装する必要ありか?
- 1ユーザーが複数のプロバイダ経由で認証された場合、どのような挙動になるのか?
Next.js x Prisma x NextAuth.jsの環境を構築する
Next.jsのセットアップ
Next.jsのTypeScriptサンプルを利用し、プロジェクトを作成
npx create-next-app nextjs-auth-prisma-poc --use-npm --example "https://github.com/vercel/next-learn/tree/master/basics/typescript-final"
MySQLのセットアップ
MySQLにデータベース:pocを作成
CREATE DATABASE poc
Prismaのセットアップ
Prismaをインストール
npm install prisma --save-dev
Prismaを初期化し、必要なファイルを自動生成する。
npx prisma init
下記の2点が実行される
- データベース接続変数とスキーマモデルを含むPrismaスキーマを含むschema.prismaというファイルを含むprismaという新しいディレクトリを作成します。
- プロジェクトのルートディレクトリに .env ファイルを作成します。このファイルは、環境変数 (データベース接続など) を定義するために使用されます。
Prismaの設定ファイル更新
MySQLを使用するので、Prismaのプロバイダ設定をmysqlに変更
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
データベース接続設定を変更
DATABASE_URL="mysql://root:root@localhost:3306/poc?schema=public"
Prismaクライアントをインストール
npm install @prisma/client
Prisma Clientは、あなたのデータに合わせて自動生成され、タイプセーフなクエリビルダです。
NextAuthのセットアップ
NextAuthとPrismaアダプターをインストール
npm install next-auth @next-auth/prisma-adapter
NextAuthのCredentials認証設定
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default NextAuth({
providers: [
CredentialsProvider({
// サインインフォームに表示する名前 (例: "Sign in with...")
name: "Credentials POC",
// 認証情報は、サインインページに適切なフォームを生成するために使用されます。
// 送信されることを期待するフィールドを何でも指定することができます。
// 例: ドメイン、ユーザー名、パスワード、2FAトークンなど。
// オブジェクトを通して、任意の HTML 属性を <input> タグに渡すことができます。
credentials: {
username: { label: "ユーザー名", type: "text", placeholder: "ユーザー名" },
password: { label: "パスワード", type: "password" }
},
async authorize(credentials, req) {
const { username, password } = credentials
// ここにロジックを追加して、提供されたクレデンシャルからユーザーを検索します。
const user = { id: 1, name: "太郎", email: "tarou@example.com" }
if (user) {
// 返されたオブジェクトはすべて、JWTの `user` プロパティに保存されます。
return user
} else {
// もし、NULLを返した場合は、ユーザーに詳細を確認するよう促すエラーが表示されます。
return null
// また、このコールバックをエラーで拒否することもできます。この場合、ユーザーはエラーメッセージをクエリパラメータとして持つエラーページに送られます。
}
}
}),
],
})
NextAuthで利用する認証のためのPrisma Schema設定
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
DBマイグレーション
PrismaSchemaを元にSQLのマイグレーションファイルを作成し、実行する。
npx prisma migrate dev
Prismaクライアントライブラリ生成
PrismaSchemaを読み込み、Prismaクライアントライブラリを生成する
npx prisma generate
NextAuthログイン導線作成
SessionProviderの配置
useSession を使えるようにするに、まずアプリケーションのトップレベルでセッションコンテキストである <SessionProvider /> を設定する。
import '../styles/global.css'
import { AppProps } from 'next/app'
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
useSessionを利用したログイン導線
useSession()を使って、サインインしているかどうかを確認する
import Head from 'next/head'
import Layout from '../components/layout'
import { useSession, signIn, signOut } from "next-auth/react"
export default function Home() {
return (
<Layout home>
<Head>
<title>POC</title>
</Head>
<SessionComponent />
</Layout>
)
}
export function SessionComponent() {
const { data: session } = useSession()
if (session) {
return (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)
}
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)
}
不明点1・2の検証
- 永続化されないと書いているが、別途DBで管理するようにロジックを挟み込めば問題ないか?
- パスワード再設定や新規登録は丸々実装する必要ありか?
ロジックを挟み込むのではなく、アカウント管理自体は自ら実装する必要がある。
NextAuthがCredentialで提供してくれるのは、JWTによるセッション管理。
1ユーザーが複数のプロバイダ経由で認証された場合、どのような挙動になるのか?
1人のユーザーが複数のアカウントを持つことはできますが、各アカウントが持つことができるユーザーは1人に限られます。
大丈夫そう
アカウント管理実装
ユーザーモデルにパスワードカラムを追加
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
crypted_password String? @db.VarChar(255)
accounts Account[]
sessions Session[]
}
保存する暗号化パスワードを生成するbcryptをインストール
npm install bcrypt
テストユーザーをDBに挿入するseedファイルを実装
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcrypt'
const prisma = new PrismaClient()
async function main() {
const saltRounds = 10;
const password = "test"
const hashedPassword = await bcrypt.hash(password, saltRounds)
const testUser = await prisma.user.upsert({
where: { email: 'test@test.com' },
update: {},
create: {
email: 'test@test.com',
name: 'テスト',
hashedPassword
},
})
console.log({ testUser })
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
seedファイルを実行するためのts-nodeをインストール
npm install -D typescript ts-node @types/node
package.jsonにseedコマンドを設定
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
シード実行
npx prisma db seed