Nuxt3でNuxt-Authモジュールを使って認証処理をつくる

shallow focus photography of smartphone NuxtJS
Photo by cottonbro studio on Pexels.com
この記事は約18分で読めます。

以前、以下の投稿で簡易な認証機能を試してみました。しかし、以下は認証フローのイメージを掴むための簡易なサンプルでしたので機能は不十分でした。

Nuxt3での簡易の認証フローを構築する
Nuxt3でアクセストークンを使った簡易な認証フローの構築をしてみます。簡易な認証フローでは、route middlewareを使います。route middlewareはNuxt2にも同様にあった仕組みです。サーバーサイド(SSRサーバ初...

今回は、Nuxt-Authというモジュールを使って認証機能を組み込むための方法について見ていきたいと思います。

環境

  • macOS Catalina (intel cpu)
  • nuxt 3.4.1
  • next-auth 4.22.0
  • @sidebase/nuxt-auth 0.5.0-rc.0
  • Node.js 18.15.0

Nuxt-Authモジュール

Nuxt3にあるNuxt-Authモジュールは、NextAuth.jsというNext.jsアプリケーション用に書かれた認証ライブラリをベースにNuxt3用に提供されているモジュールになっています。nuxt/authという類似名のモジュールがありますが、こちらはNuxt公式でNuxt3にbuilt-inで認証機能を追加するためにNuxt Layersという機能を使って開発されているようですが、まだworkingステータスとなっているようです(本記事執筆時点、まだNuxt2の頃のまま?)。その間は、本記事にあるNuxt-Authモジュールの利用を推奨する、となっています。

Nuxt-Authモジュールは非常に多くの認証プロバイダをサポートしています。NextAuth.jsのサイトを見るとそのサポートの多さが分かります。ユーザの方が使われているだろうサービスプロバイダで困ることはなさそうです。

Nuxt-Authを使ったサンプル

Nuxt-Authモジュールをインストールして認証機能を実装していきたいと思います。今回はGoogleとGitHubプロバイダを使用してみます。

インストール

モジュールのインストールをします。@sidebase/nuxt-authは、0.5.0-rc.0の版となっているようです。

pnpm i -D @sidebase/nuxt-auth
pnpm i -D next-auth@4.22.0

続いてnuxt.configでモジュールの設定をします。以下は、全てのページでデフォルトで認証を有効にする設定をしています。

export default defineNuxtConfig({
  modules: ["@sidebase/nuxt-auth"],
  auth: {
    enableGlobalAppMiddleware: true, // サイト全体で認証を必要にする
  },
});

その他の設定は公式ドキュメントで参照できます。

参考 https://sidebase.io/nuxt-auth/configuration/nuxt-config

認証機能の作成

/api/auth/[...].tsCatch-all Routeにハンドラーの定義を書くようになっています。Catch-all Routeは、リクエストが配下のパスにマッチしない場合にデフォルトで呼ばれるルートです。

参考 https://nuxt.com/docs/guide/directory-structure/pages/#catch-all-route

providersというプロパティに認証プロバイダを設定します。Googleプロバイダを設定した例は以下のようになります。

server/api/auth/[…].ts

import GoogleProvider from "next-auth/providers/google";

import { NuxtAuthHandler } from "#auth";

export default NuxtAuthHandler({
  providers: [
    // @ts-expect-error
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
});

providersの配列には複数のプロバイダの設定が可能です。GoogleやGitHubといった複数のプロバイダを設定するとそれらプロバイダを選択できる認証画面が表示されます。GitHubも追加してみましょう。

import GithubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'

import { NuxtAuthHandler } from '#auth'

export default NuxtAuthHandler({
  providers: [
    // @ts-expect-error
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    // @ts-expect-error
    GithubProvider.default({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
})

アプリケーションを起動しhttp://localhost:3000にアクセスすると以下のような認証画面に飛ばされることを確認できます。

app.vue

<template>
  <div>
    <NuxtPage />
  </div>
</template>

pages/index.vue

<template>
  <div>
    <slot />
  </div>
</template>

GoogleまたはGitHubを選択できる認証画面です。

認証プロバイダの設定

GoogleとGitHubのプロバイダを登録して実際に設定してみます。callback URLは、デフォルトでは/api/auth/callback/:providerとなっています。

Google

Google API ConsoleからクライアントID、シークレットを取得します。

Google API Consoleへ移動

プロジェクト作成等の詳細は割愛します。

 

GitHub

Developer Settingsに移動

上記の登録を完了し、.envファイルにそれぞれプロバイダのクライアントIDとシークレットを設定しアプリケーションを起動します。アプリケーションの認証画面で認証プロバイダのリンクをクリックすると以下のような認証プロバイダの要求画面が表示されます。

Google GitHub

認証によるページ保護

2つの設定方法があります。1つはサイト全体で保護するグローバル、もう1つはページごとに保護するローカルです。

グローバルで保護

サイト全体で保護する場合は、モジュールのオプションで設定できます。個別のページで認証をかけたくない場合は、definePageMetaで設定することができます。

先ほどの例の通りモジュールのオプションで設定します。

export default defineNuxtConfig({
  modules: ["@sidebase/nuxt-auth"],
  auth: {
    enableGlobalAppMiddleware: true, // サイト全体で認証を必要
  },
});

個別のページで認証を要求しない場合は以下のようにします。

<script setup lang="ts">
definePageMeta({ auth: false })
</script>

ローカルで保護

definePageMeataで設定します。middlewareによって保護されます。

<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
</script>

Guest Mode

Guest Modeを使うと、認証済みユーザは認証ページを開かずredirectページに遷移させるといったフローを構築できます。自分で認証済みか否かをチェックすることもできますが、ライブラリ側で制御してくれるのは嬉しいと思います。

<script setup lang="ts">
definePageMeta({
  auth: {
    unauthenticatedOnly: true, // 認証済みでない場合は参照できる
    navigateAuthenticatedTo: '/profile', // 認証済みの場合に遷移させるページ、指定なしは'/'
  },
})
</script>

カスタム認証ページ

デフォルトで認証画面が用意されているのですぐに始めることができますが、通常はWebサイトのデザインに合わせて画面デザインすることが多いと思います。そのため、個別の認証ページに飛ばすことができます。これはNuxtAhtuHandlerで設定します。

server/api/auth/[…].ts

import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";

import { NuxtAuthHandler } from "#auth";

export default NuxtAuthHandler({
  pages: {
    signIn: "/login", // 用意したログインページに設定
  },
  providers: [
    // @ts-expect-error
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    // @ts-expect-error
    GithubProvider.default({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
});

pages/login.vue

<script setup lang="ts">
definePageMeta({
  auth: { unauthenticatedOnly: true, navigateAuthenticatedTo: "/profile" },
});

const { signIn } = useAuth();
</script>
<template>
  <div>
    <h2>Login</h2>
    <div>
      <p>Sign-In Options:</p>
      <button @click="signIn('google')">Google</button>
      <button @click="signIn('github')">Github</button>
    </div>
  </div>
</template>

pages/profile.vue

<script setup lang="ts">
const { signOut, data } = useAuth();
</script>
<template>
  <div>
    <p>Logged In As:{{ data?.user?.email }}</p>
    <button @click="() => signOut()">Sign Out</button>
  </div>
</template>

確認してみましょう。

http://localhost:3000/ の認証が必要なページにアクセスします。

ログインページ

ログイン後の/profileページ

Sign Outすると再びログインページに戻ります。

JWTトークンの使用

sessionStrategyをjwtにするとJWTを使えます。

import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import SequelizeAdapter from "@next-auth/sequelize-adapter";

export default NuxtAuthHandler({
  pages: {
    signIn: "/login",
  },
  session: {
    strategy: "jwt", // jwtにする
  },
  providers: [
    // @ts-expect-error
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GPOOGLE_CLIENT_SECRET!,
    }),
    // @ts-expect-error
    GithubProvider.default({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
});

ログインして開発者コンソールでcookieに格納されているsession情報をのぞくと以下のようにJWTの値が入っています。

Credentialsプロバイダ+Sequelizeでのログイン処理

sequelize adapterを使うとデータベースと連携しアカウントやユーザ登録がされるようになります。

sequelize-adapterを使う方法を見ていきます。まずはモジュールをインストールします。データベースはSQLiteを使います。

pnpm i -D sequelize sqlite3 @next-auth/sequelize-adapter

NuxtAuthHandlerを以下のように修正します。

import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import SequelizeAdapter from "@next-auth/sequelize-adapter";

import { NuxtAuthHandler } from "#auth";
import { Sequelize } from "sequelize";

// データベース設定
const sequelize = new Sequelize({
  storage: "./test.sqlite",
  dialect: "sqlite",
});

export default NuxtAuthHandler({
  pages: {
    signIn: "/login",
  },
  providers: [
    // @ts-expect-error
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GPOOGLE_CLIENT_SECRET!,
    }),
    // @ts-expect-error
    GithubProvider.default({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
  // https://authjs.dev/reference/adapter/sequelize
  // @ts-expect-error
  adapter: SequelizeAdapter.default(sequelize),  // adapterの設定
});

上記ではモデルの定義をしていません。SequelizeAdapterはモデルがない場合、内部にもつモデル定義からテーブル作成をしてくれます(ただしproduction環境ではない場合)。実際にはsequelizeのsync関数が呼ばれています。production環境では通常マイグレーションする必要があります。

ではこの設定にしてログインしてみましょう。ログインすると以下のようにコンソールにテーブル作成のSQLが出力されるのを確認できます。

[19:37:33] Executing (default): CREATE TABLE IF NOT EXISTS accounts (id UUID PRIMARY KEY, type VARCHAR(255) NOT NULL, provider VARCHAR(255) NOT NULL, provider_account_id VARCHAR(255) NOT NULL, refresh_token VARCHAR(255), access_token VARCHAR(255), expires_at INTEGER, token_type VARCHAR(255), scope VARCHAR(255), id_token TEXT, session_state VARCHAR(255), user_id UUID REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE);
[19:37:33] Executing (default): CREATE TABLE IF NOT EXISTS sessions (id UUID PRIMARY KEY, expires DATETIME NOT NULL, session_token VARCHAR(255) NOT NULL, user_id UUID REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE (session_token));
[19:37:33] Executing (default): CREATE TABLE IF NOT EXISTS verification_tokens (token VARCHAR(255) PRIMARY KEY, identifier VARCHAR(255) NOT NULL, expires DATETIME NOT NULL);
[19:37:33] Executing (default): CREATE TABLE IF NOT EXISTS users (id UUID PRIMARY KEY, name VARCHAR(255), email VARCHAR(255), email_verified DATETIME, image VARCHAR(255), UNIQUE (email));

ログイン後にテーブルaccounts、users、sessionsを見るとレコードが登録されているのが分かります。

accountsテーブル

usersテーブル

sessionsテーブル

 

CredentialsProviderの使用

CredentialsProviderを使うとusernameとpasswordのような入力を使って既存のシステムを使った認証ができるようです。

import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import SequelizeAdapter from "@next-auth/sequelize-adapter";

import { NuxtAuthHandler } from "#auth";
import { Sequelize } from "sequelize";

const sequelize = new Sequelize({
  storage: "./test.sqlite",
  dialect: "sqlite",
});

// @ts-expect-error
const adapter = SequelizeAdapter.default(sequelize);

export default NuxtAuthHandler({
  pages: {
    // signIn: "/login",
  },
  session: {
    strategy: "jwt",
  },
  providers: [
    // @ts-expect-error
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GPOOGLE_CLIENT_SECRET!,
    }),
    // @ts-expect-error
    GithubProvider.default({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    // @ts-expect-error
    CredentialsProvider.default({
      name: "Credentials",
      credentials: {
        username: { label: "Username", type: "text", placeholder: "tarou" },
        password: { label: "Password", type: "password" },
      },
      async authorize(
        credentials: Record<"username" | "password", string> | undefined,
        req: any,
      ) {
        // ここに処理を書く
        // 以下はユーザがいない場合に新規に作成するというシンプルなサンプルで試している
        let user = await adapter.getUserByEmail(credentials?.username);
        if (!user) {
          user = await adapter.createUser({
            email: credentials?.username,
          });
        }
        console.log(credentials);
        console.log(req);
        return user;
      },
    }),
  ],

  // https://authjs.dev/reference/adapter/sequelize
  adapter,
});

EmailProviderの使用

EmailProviderを使うと検証用のトークンを生成した後メール認証リクエストを送信、メール受信者が認証用のURLをクリックしてユーザ登録を完了するというフローを構築できます。認証URLをコンソールに出力したのが以下です。

まとめ

Nuxt-Authモジュールを使うとNuxt3で認証プロバイダを用いた認証機能をクイックに実装することができました。また、アダプタを使うことでデータベースとの連携も簡単に行うことができます。

Nuxt-Authはまだrc版ですが、サクッと導入できプロバイダやアダプタなど機能も豊富でとても使いやすいため、今後Nuxt3でアプリケーション構築していくのに使っていきたいと思いました。

参考リンク

コメント

タイトルとURLをコピーしました