Open5

NextAuthでID/PASSのログイン認証を実装する

kkawanabeekkawanabee

https://next-auth.js.org/
公式を読んだ上で不明点を実装しながら検証する。

検証したい要件

  • ログイン認証はID/PASS
  • ユーザーのGmailと連携し、GmailAPIを叩けるようにする
    • 今後SFなど他のサービスとも連携するため、本サービスの認証はID/PASSを利用
  • FW:Next.js, ORM:Prismaを利用予定

理解したこと

  • NextAuthは、OAuthを推してる。
  • ID/PASSログイン認証は、Credentialsプロバイダで実装する。(あんまり推奨していない)

Credentialsプロバイダは、ユーザ名とパスワード、二要素認証、ハードウェアデバイス(YubiKey U2F / FIDOなど)など、任意の認証情報を使ってサインインすることを可能にします。

https://next-auth.js.org/configuration/providers/credentials

不明点

  • 永続化されないと書いているが、別途DBで管理するようにロジックを挟み込めば問題ないか?

これは、ユーザーを認証する必要がある既存のシステムがある場合の使用をサポートすることを目的としています。
認証情報プロバイダは、セッションで JSON ウェブ トークンが有効になっている場合にのみ使用できます。認証情報プロバイダで認証されたユーザーは、データベースに永続化されません。

  • パスワード再設定や新規登録は丸々実装する必要ありか?
  • 1ユーザーが複数のプロバイダ経由で認証された場合、どのような挙動になるのか?
kkawanabeekkawanabee

Next.js x Prisma x NextAuth.jsの環境を構築する

Next.jsのセットアップ

https://nextjs.org/learn/basics/create-nextjs-app/setup

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のセットアップ

https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project/relational-databases-typescript-postgres

Prismaをインストール

npm install prisma --save-dev

Prismaを初期化し、必要なファイルを自動生成する。

npx prisma init

下記の2点が実行される

  1. データベース接続変数とスキーマモデルを含むPrismaスキーマを含むschema.prismaというファイルを含むprismaという新しいディレクトリを作成します。
  2. プロジェクトのルートディレクトリに .env ファイルを作成します。このファイルは、環境変数 (データベース接続など) を定義するために使用されます。

Prismaの設定ファイル更新

MySQLを使用するので、Prismaのプロバイダ設定をmysqlに変更

prisma/shcema.prisma
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

データベース接続設定を変更

.env
DATABASE_URL="mysql://root:root@localhost:3306/poc?schema=public"

Prismaクライアントをインストール

npm install @prisma/client

Prisma Clientは、あなたのデータに合わせて自動生成され、タイプセーフなクエリビルダです。

https://www.prisma.io/docs/concepts/components/prisma-client

NextAuthのセットアップ

https://next-auth.js.org/adapters/prisma

NextAuthとPrismaアダプターをインストール

npm install next-auth @next-auth/prisma-adapter

NextAuthのCredentials認証設定

pages/api/auth/[...nextauth].ts
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

          // また、このコールバックをエラーで拒否することもできます。この場合、ユーザーはエラーメッセージをクエリパラメータとして持つエラーページに送られます。
        }
      }
    }),
  ],
})

https://next-auth.js.org/providers/credentials

NextAuthで利用する認証のためのPrisma Schema設定

shcema.prisma
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クライアントライブラリ生成

https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project/relational-databases/install-prisma-client-typescript-postgres
PrismaSchemaを読み込み、Prismaクライアントライブラリを生成する

npx prisma generate

kkawanabeekkawanabee

NextAuthログイン導線作成

https://next-auth.js.org/getting-started/example

SessionProviderの配置

useSession を使えるようにするに、まずアプリケーションのトップレベルでセッションコンテキストである <SessionProvider /> を設定する。

pages/_app.tsx
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()を使って、サインインしているかどうかを確認する

index.tsx
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>
    </>
  )
}

kkawanabeekkawanabee

不明点1・2の検証

  1. 永続化されないと書いているが、別途DBで管理するようにロジックを挟み込めば問題ないか?
  2. パスワード再設定や新規登録は丸々実装する必要ありか?

ロジックを挟み込むのではなく、アカウント管理自体は自ら実装する必要がある。
NextAuthがCredentialで提供してくれるのは、JWTによるセッション管理。

1ユーザーが複数のプロバイダ経由で認証された場合、どのような挙動になるのか?

https://next-auth.js.org/adapters/models

1人のユーザーが複数のアカウントを持つことはできますが、各アカウントが持つことができるユーザーは1人に限られます。

大丈夫そう

kkawanabeekkawanabee

アカウント管理実装

ユーザーモデルにパスワードカラムを追加

schema.prisma
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ファイルを実装

prisma/seed.ts
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コマンドを設定

package.json
"prisma": {
   "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},

シード実行

npx prisma db seed