Nuxt 3で認証機能を実装したい場合に利用できるライブラリ/サービスは複数存在します。それらのライブラリの中から2023年11月にリリースしたv0.6で新たにlocal Providerをサポートした@sidebase/nuxt-authの認証機能を利用して動作確認を行います。設定だけではなくより深くライブラリの実装を理解するために一部ソースコードも確認しています。2024年7月の最新バージョンはv0.8です。

local ProviderはOAuthを利用した認証ではなくusernameとpasswordを利用したログイン認証で内部ではTokenとCookieを利用しています。@sidebase/nuxt-authでOAuthを利用した認証機能を実装したい場合にはnext-auth(auth.js)を追加インストールする必要がありますがlocal Providerであれば@sidebase/nuxt-auth単独で認証機能を実装することができます。

sidebaseのページではlocal Providerの一般的な利用方法としてはすでにcredential(username+passwordなど)を持ったバックエンドを持っている場合、staticアプリケーション(nuxi generate)を構築したい場合を想定しているようです。

ドキュメントのIntroductionでは@sidebase/nuxt-authで提供するProviderの機能の違いなども掲載されているので利用する場合はぜひ参考にしてみてください。

認証の流れ

設定を行う前にsidebase/nuxt-authのlocal Providerでは認証管理をどのように行っているのか簡単に説明を行っておきます。

  • ログイン画面で登録済みのusename+passwordを入力して”sign in”ボタンをクリック。
  • ライブラリが持つuseAuth Composableから戻される SignIn関数の引き引数にCredentials(username+password)を入れてサインイン処理を開始。
  • /api/auth/loginエンドポイントにCredentialsを含んだPOSTリクエストを送信。Credentialsを利用してユーザの存在をチェック。
  • ユーザの存在確認後、ユーザ情報を含んだTokenを新規作成、作成したToken返却。
  • 返却されたTokenを保存。Tokenを持つCookieを作成。
  • TokenをHeaderに設定し、/api/auth/sessionエンドポイントにGETリクエストが送信。
  • 送信されてきたTokenをHeaderから取り出し、Tokenの検証。
  • Tokenの検証が完了後、Tokenから取り出したユーザ情報を返却。
  • 返却されたユーザ情報を保存(useState Composable利用)。ログイン完了(authenticated)。
  • ページを移動する度middlewareが実行されuseState Composableを介してユーザ情報が存在するかチェック。
  • getSessionを実行してTokenの検証を行いTokenの期限切れをチェック。切れていればユーザ情報、Cookieを削除(unAuthenticated)。切れていなければログイン状態を継続(authenticated)。

プロジェクトの作成

認証機能の動作確認を行うためにNuxt 3のプロジェクトを作成します。プロジェクト名はnuxt3-authとしています。


% npx nuxi@latest init nuxt3-auth

✔ Which package manager would you like to use?
npm
◐ Installing dependencies...                                                                                       16:51:08

> postinstall
> nuxt prepare

✔ Types generated in .nuxt                                                                                        16:52:13

added 740 packages, and audited 742 packages in 1m

122 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
✔ Installation completed.                                                                                         16:52:13

✔ Initialize git repository?
No
                                                                                                                   16:53:08
✨ Nuxt project has been created with the v3 template. Next steps:
 › cd nuxt3-auth                                                                                                   16:53:08
 › Start development server with npm run dev           

プロジェクトを作成後、プロジェクトディレクトリに移動して@sidebase/nuxt-authのインストールを行います。


% npm i -D @sidebase/nuxt-auth      

インストールしたNuxt 3とnuxt-authのバージョンをpackage.jsonファイルで確認しておきます。@sidebase/nuxt-authのバージョンは0.6以上であることが確認できます。


{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "@sidebase/nuxt-auth": "^0.8.1",
    "nuxt": "^3.12.4",
    "vue": "latest"
  }
}

動作確認のための準備

認証機能を実装する前に動作確認で利用するページの作成を行っておきます。

layoutsの作成

すべてのページの共有部分をLayoutファイルで設定を行うためLayoutの設定を行います。layoutsディレクトリを作成してdefault.vueファイルを作成します。3つのページを作成するのでLayoutファイルには3つのページを行き来できるようにNuxtLinkでリンクを設定しておきます。


<template>
  <div>
    <nav>
      <ul>
        <li><NuxtLink to="/">Home</NuxtLink></li>
        <li><NuxtLink to="/login">Login</NuxtLink></li>
        <li><NuxtLink to="/dashboard">Dashboard</NuxtLink></li>
      </ul>
    </nav>
    <slot />
  </div>
</template>

app.vueファイルを更新して作成したLayoutファイルを利用できるようにNuxtLayoutタグとNuxtPageタグの設定を行います。


<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

ページファイルの作成

ページファイルを作成するためにpagesディレクトリを作成して、index.vue, login.vueファイルを作成します。


<template>
  <h1>Home Page</h1>
</template>

<template>
  <h1>Login Page</h1>
</template>

dashboardページについてはdashboardディレクトリを作成してその下にindex.vueファイルを作成します。


<template>
  <h1>Dashboard</h1>
</template>

最終的には3つのページはそれぞれ異なる認証によるアクセス制限を行います。

  • index.vueファイルはログイン前、後に関わらずいつでもアクセスできるページ
  • login.vueファイルはログイン前にはアクセスできるがログインが完了するとアクセスできないページ
  • dashboard.vueファイルはログイン後のみアクセスできるページ

“npm run dev”コマンドを実行して開発サーバを起動します。


 % npm run dev

> dev
> nuxt dev

Nuxt 3.12.4 with Nitro 2.9.7                                  13:44:10
                                                              13:44:11
  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose

  ➜ DevTools: press Shift + Option + D in the browser (v1.3.9)13:44:13

✔ Vite client built in 47ms           13:44:14
✔ Vite server built in 471ms          13:44:14
✔ Nuxt Nitro server built in 640 ms   nitro 13:44:15
ℹ Vite client warmed up in 0ms        13:44:15
ℹ Vite server warmed up in 726ms      13:44:16

http://localhost:3000にアクセスすると以下のページが表示されます。リンクをクリックすると各ページにアクセスすることができます。

初期状態のページ構成
初期状態のページ構成

認証設定

モジュールの設定

インストールした@sidebase/nuxt-authはモジュールとしてnuxt.config.tsファイルに登録する必要があります。nuxt.config.tsファイルにはmodulesの設定とauthの設定を行います。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2024-04-03',
  devtools: { enabled: true },
  modules: ['@sidebase/nuxt-auth'],
  auth: {
    provider: {
      type: 'local',
    },
  },
});

providerのtypeにはauthjs, local, refreshの3つのいずれかを指定することができますが本書ではlocal-auth-providerの動作確認を行うので”local”を設定しています。

localを利用する場合はusernameとpasswordを利用した認証でエンドポイントにリクエストを送信してユーザ認証が完了するとTokenを戻すように設定を行う必要があります。refreshはlocalにリフレッシュトークンの機能を追加したものです。authjsはnext-authの追加インストールが必要になります。Oauthの認証を含め、next-authの機能を利用することができます。

nuxt.config.tsファイルの更新が完了したら開発サーバを起動したターミナルにはnuxt-authのsetupの開始と完了のメッセージを確認することができます。メッセージが表示されればnuxt-authのnuxt.config.tsでの初期設定に問題がないということになります。メッセージからAuth APIのlocationとして/api/authが関係することがわかります。


//略
ℹ nuxt-auth setup starting                                 sidebase-auth 7:34:11
ℹ Selected provider: local. Auth API location is /api/auth sidebase-auth 7:34:11
✔ nuxt-auth setup done
//略

nuxt.config.tsファイルの設定後もアプリケーションには何も変化はなくページ間の移動も自由に行うことができます。nuxt-authを利用することができるがその機能を利用していない状態です。

アプリケーション全体に認証によるアクセスの制限を行うためにglobalAppMiddlewareの設定を行います。名前にもmiddlewareが含まれている通り、Nuxtのmiddlewareの機能を利用します。middlewareはページのアクセス前に処理を追加することができ、認証でmiddlewareを利用することでページのアクセス前にアクセスしているユーザが認証済みかどうかチェックします。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ['@sidebase/nuxt-auth'],
  auth: {
    globalAppMiddleware: true,
    provider: {
      type: 'local',
    },
  },
});
nuxt.config.tsファイルの中でグローバルでアプリケーション全体に認証によるアクセス制限を行わず、個別のページでアクセス制限を行うこともできます。その際はページの中で認証設定を行います。
fukidashi

nuxt.config.tsファイルにglobalAppMiddlewareの設定を行なった後に”/”, “/dashboard”にアクセスすると”/login”にリダイレクトされます。アプリケーション全体に認証によるアクセス制限が設定されどのページにアクセスしてもloginページが表示されます。

Moduleオプションの確認

なぜ/loginページのみアクセス可能なのかということを理解するためにはnuxt.config.tsファイルで設定できるモジュールオプションの内容を理解する必要があります。

globalAppMiddlewareもnuxt.config.tsファイルで設定可能なModuleオプションの一つです。
fukidashi

@sidebase/nuxt-authのドキュメントのConfigurationを確認します。スクロールするとglobalAppMiddlewareの設定についての説明も記述されているので参考になります。


globalAppMiddleware
Type: GlobalMiddlewareOptions | boolean
Default: false
Whether to add a global authentication middleware that protects all pages. Can be either false to disable, true to enable with defaults or an object to enable with provided options.

If you enable this, everything is going to be protected and you can selectively disable protection for some pages by specifying definePageMeta({ auth: false })
If you disable this, everything is going to be public and you can selectively enable protection for some pages by specifying definePageMeta({ auth: true })

globalAppMiddlewareをtrueにした場合はすべてのページが制限されるので制限をdisableにする際には各ページのdefinePageMetaの引数でauth:falseを設定する必要があります。つまりアクセスにより制限を行いたくないページにはdefinePageMetaの引数でauth:falseを設定する必要があります。

/loginへのリダイレクトを設定する前にドキュメントの設定に従って”/”ページ(pages/index.vue)に対する認証によるアクセス制限を停止します。


<script setup>
definePageMeta({ auth: false });
</script>
<template>
  <h1>Home Page</h1>
</template>

“/”にアクセスすると”/login”にリダイレクトされることはなくなりました。/dashboardへのアクセスは引き続き/loginへリダイレクトされます。

/loginへのリダイレクトの設定に戻ります。Providerのオプションで設定することができるのでドキュメントではLocal/Refresh Providerの説明(https://auth.sidebase.io/guide/local/quick-start)に記述されています。

スクロールするとpagesの設定がありpagesのloginで設定することができます。

/loginの設定
/loginの設定

pagesははログインしていない状態でアクセス制限されたページにアクセスした時にリダイレクトさせるページを設定するオプションです。設定するページはglobalmiddlewareでブロックされないページである必要があります。

pagesの動作確認のため存在しないページですがpagesの値をsigninに変更してみましょう。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2024-04-03',
  devtools: { enabled: true },
  modules: ['@sidebase/nuxt-auth'],
  auth: {
    globalAppMiddleware: true,
    provider: {
      type: 'local',
      pages: {
        login: '/signin',
      },
    },
  },
});

設定後、”/dashboard”, “/login”にアクセスすると”/sigin”にリダイレクトされます。signinページが存在しないので”404 Not Found”エラーが表示されます。pagesオプションによってリダイレクト先が設定されることがわかりました。

signinページへのリダイレクトの確認
signinページへのリダイレクトの確認

“/login”にリダイレクトする設定が確認できたのでnuxt.config.tsファイルを元の設定に戻します。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2024-04-03',
  devtools: { enabled: true },
  modules: ['@sidebase/nuxt-auth'],
  auth: {
    globalAppMiddleware: true,
    provider: {
      type: 'local',
    },
  },
});

/loginにリダイレクトされることはわかりましたがまだもう一つ疑問が残ります。login.vueではindex.vueのようにdefinePageMetaでauth:falseに設定していないのにログインしていない状態でページにアクセスができるのでしょう。この動作を理解するためにはmiddlewareのコードを確認する必要があります。

@sidebase/nuxt-auth/dist/runtime/middlewareディレクトリにauthモジュールで利用しているauth.mjsファイルがあります。auth.mjsファイルの中ではdefineNuxtRouteMiddlewareでユーザがアクセスしたページへのアクセスを許可するかどうかの分岐がいくつもありますがその中の一つに以下の分岐があります。


if (authConfig.provider?.type === "local" || authConfig.provider?.type === "refresh") {
  const loginRoute = authConfig.provider?.pages?.login;
  if (loginRoute && loginRoute === to.path) {
    return;
  }
}

providerがlocalの場合にモジュールオプションのpagesのloginの値が存在してto.path(アクセス先)が/loginの場合はそのままページにアクセス可能となっています。つまりpagesで設定したリダイレクト先はアクセス制限されるページから除外されるように設定が行われています。

@sidebase/nuxt-auth/distはnode_modulesの下にあります。node_modulesの下かGitHubのページで確認してください。
fukidashi

デフォルトのModuleオプションの確認

nuxt.config.tsファイルで設定したオプションはglobalAppMiddlewareとproviderのtypeのみです。その他のオプションの値はどこに設定されているのでしょう。

@sidebase/nuxt-auth/distディレクトリのmodule.mjsファイルに記述されています。開発サーバ起動時のメッセージもこのファイルの中に記述されています。


const topLevelDefaults = {
  isEnabled: true,
  session: {
    enableRefreshPeriodically: false,
    enableRefreshOnWindowFocus: true
  },
  globalAppMiddleware: {
    isEnabled: false,
    allow404WithoutAuth: true,
    addDefaultCallbackUrl: true
  }
};
const defaultsByBackend = {
  local: {
    type: "local",
    pages: {
      login: "/login"
    },
    endpoints: {
      signIn: { path: "/login", method: "post" },
      signOut: { path: "/logout", method: "post" },
      signUp: { path: "/register", method: "post" },
      getSession: { path: "/session", method: "get" }
    },
    token: {
      signInResponseTokenPointer: "/token",
      type: "Bearer",
      headerName: "Authorization",
      maxAgeInSeconds: 30 * 60,
      sameSiteAttribute: "lax"
    },
    sessionDataType: { id: "string | number" }
  },
  refresh: {
    type: "refresh",
    pages: {
      login: "/login"
    },
    refreshOnlyToken: true,
    endpoints: {
      signIn: { path: "/login", method: "post" },
      signOut: { path: "/logout", method: "post" },
      signUp: { path: "/register", method: "post" },
      getSession: { path: "/session", method: "get" },
      refresh: { path: "/refresh", method: "post" }
    },
    token: {
      signInResponseTokenPointer: "/token",
      type: "Bearer",
      headerName: "Authorization",
      maxAgeInSeconds: 5 * 60,
      sameSiteAttribute: "none"
      // 5 minutes
    },
    refreshToken: {
      signInResponseRefreshTokenPointer: "/refreshToken",
      maxAgeInSeconds: 60 * 60 * 24 * 7
      // 7 days
    },
    sessionDataType: { id: "string | number" }
  },
  authjs: {
    type: "authjs",
    trustHost: false,
    // @ts-expect-error
    defaultProvider: void 0,
    addDefaultCallbackUrl: true
  }
};

Nuxt 3のバージョンによってはNuxt Devtoolsをデフォルトから利用できるのでDevtoolsのRuntime Configから確認することができます。

DevtoolsのRunTimeConfigの確認
DevtoolsのRunTimeConfigの確認

ここまでの設定でページを制限するmiddlewareの設定方法とモジュールのオプションにはどのような設定値があるのかついて理解することができました。

ここから実際に認証機能の実装を行なっていきます。

useAuth Composable

現在の認証の状態、Sessionに保存されているデータの確認、SignIn, SignOut関数はすべてuseAuth Composableを利用します。localの場合のuseAuth Composableから戻される変数と関数は以下の通りです。


const {
  status,
  data,
  token,
  lastRefreshedAt,
  getSession,
  signUp,
  signIn,
  signOut,
} = useAuth()

status

現在の認証状態の確認はuseAuth Composableから戻されるstatusで確認することができるのでindex.vueファイルで設定します。


<script setup>
definePageMeta({ auth: false });
const { status } = useAuth();
</script>
<template>
  <h1>Home Page</h1>
  <p>Status: {{ status }}</p>
</template>

認証が完了していないのでブラウザ上には”unauthenticated”が表示されます。

認証の状態の確認
認証の状態の確認

ログイン画面の作成(signIn)

useAuth Composableから戻されるsignIn関数を利用して認証の処理を行います。signIn関数の引数には各アプリケーションでの認証に必要なcredentialsを設定することができます。本文書ではusenameとpasswordを利用して認証を行うので入力フォームではusenameとpasswordを入力できるように設定します。ログイン画面は準備されているわけではないので各自が作成する必要があります。

ログイン画面をブラウザ上に表示するために以下のコードをlogin.vueファイルに記述します。入力フォームのusernameとpasswordを入力して”sign in”ボタンをクリックするとsignIn関数が実行されます。


<script setup>
import { ref } from 'vue';

const { signIn } = useAuth();

const username = ref('');
const password = ref('');
</script>
<template>
  <div>
    <h1>Login Page</h1>
    <form @submit.prevent="signIn({ username, password })>
      <div>
        <label for="username">UserName:</label>
        <input
          v-model="username"
          type="text"
          placeholder="Username"
          id="username"
        />
      </div>
      <div>
        <label for="password">Password:</label>
        <input
          v-model="password"
          type="password"
          placeholder="Password"
          id="password"
        />
      </div>
      <button type="submit">sign in</button>
    </form>
  </div>
</template>

/loginにアクセスするとログイン画面が表示されます。

ログイン画面の表示
ログイン画面の表示

UserNameとPasswordを入力して”sign in”ボタンを押してください。ブラウザのコンソールには以下のメッセージが表示されます。


POST http://localhost:3000/api/auth/login 404 (Page not found: /api/auth/login)

開発サーバを起動したターミナルにはWarningが表示され同様の内容が表示されます。


[Vue Router warn]: No match found for location with path "/api/auth/login"

上記のメッセージからsignIn関数を実行すると内部で/api/auth/loginへのPOSTリクエストが送信されることがわかります。送信先の/api/auth/loginでCredentialのチェックを行う処理を記述します。

signIn関数の内部で実行されるPOSTリクエストがどのエンドポイントに送信するかもnuxt.config.tsファイルでのモジュールオプションで設定を行うことができます。”local”のデフォルトの値を確認するとbaseURLが/api/auth、endpointsのsignInでのpathの値が”login”、methodが”post”に設定されています。そのためsignInを実行すると/api/auth/loginにPOSTリクエストが送信されることになります。

どの場所にリクエストを送信するかの処理は@sidebase/next-auth/dist/runtime/composables/local/useAuth.mjsファイルのsignIn関数の中に記述されています。


const signIn = async (credentials, signInOptions, signInParams) => {
  const nuxt = useNuxtApp();
  const runtimeConfig = await callWithNuxt(nuxt, useRuntimeConfig);
  const config = useTypedBackendConfig(runtimeConfig, "local");
  const { path, method } = config.endpoints.signIn;
  const response = await _fetch(nuxt, path, {
    method,
    body: credentials,
    params: signInParams ?? {}
  });

loginエンドポイントの作成

/api/auth/loginのエンドポイントを作成するためにserverディレクトリにapiディレクトリ、その下にauthディレクトリを作成してlogin.tsファイルを作成します。login.tsファイルの中身は各自で作成する必要があり戻り値にTokenを含める必要があります。

JWTの設定

login.tsファイルからの戻り値に含まれるTokenを作成するためにjsonwebtokenライブラリのインストール、POSTリクエストで送信したデータをバリデーションするためにバリデーションライブラリのZodのインストールを行います。jsonwebtokenは型情報のインストールも行います。Token作成、バリデーションライブラリは自由に選択ができ、バリデーションライブラリのインストールは必須ではありません。


% npm install zod
% npm install jsonwebtoken
% npm install -D @types/jsonwebtoken

jsonwebtokenとzodのインストール後のpackage.jsonファイルは下記の通りです。


{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "dependencies": {
    "@sidebase/nuxt-auth": "^0.8.1",
    "jsonwebtoken": "^9.0.2",
    "nuxt": "^3.12.4",
    "vue": "latest",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/jsonwebtoken": "^9.0.6"
  }
}

login.post.tsファイルのコードはhttps://github.com/sidebase/nuxt-auth/tree/main/playground-local/server/api/authを参考に行っています。


import z from 'zod';
import { sign } from 'jsonwebtoken';

export const SECRET = 'dummy';

export default eventHandler(async (event) => {
  const result = z
    .object({ username: z.string().min(1), password: z.literal('password') })
    .safeParse(await readBody(event));
  if (!result.success) {
    throw createError({
      statusCode: 403,
      statusMessage: 'Unauthorized, hint: try `password` as password',
    });
  }

  const expiresIn = 15;
  const { username } = result.data;
  const user = {
    username,
  };

  const accessToken = sign(user, SECRET, {
    expiresIn,
  });

  return {
    token: accessToken,
  };
});

バリデーションライブラリのzodを利用してusernameは空でないこと、passwordは”password”という文字列であることをスキーマ定義しています。リクエストで受け取ったデータをzodのsafeParseメソッドでバリデーションを行っています。バリデーションに失敗した場合はresultのsuccessプロパティの値がfalseになります。成功した場合にはsuccessはtrueになり、バリデーションをパスしたデータがresultに含まれます。

usernameが1文字以上でパスワードがpasswordであればバリデーションはパスします。

通常はバリデーションでpasswordの文字列のチェックなど行わず、バリデーションが完了した後に送信されてきたCredentialsがデータベースに保存されているユーザ情報と一致するかの確認などの追加コードが必要となりますが本文書では省略しています。実際に利用する際は必須です。

バリデーションを通過するとjsonwebtokenにsign関数を利用してTokenを生成しています。有効時間を15秒に設定しています。Tokenの中にはusernameを含めています。Tokenにはusername以外の情報も含めることができますがTokenに保存した情報はだれでも中身を見ることができるので機密情報を入れてはいけません。

最後に生成したTokenをAccess Tokenとして戻していますがオブジェクトとして戻すデータ形式もnuxt.config.tsのモジュールオプションのsignInResponseTokenPointerで設定することができます。デフォルトの”/token”を利用するのでtokenプロパティを持つオブジェクトに生成したAccess Tokenを設定して戻しています。

例えば下記のようにsignInResponseTokenPointerを設定した場合にどのようなエラーが戻されるか確認します。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ["@sidebase/nuxt-auth"],
  auth: {
    globalAppMiddleware: true,
    provider: {
      type: "local",
      token: {
        signInResponseTokenPointer: "/tokens/accessToken",
      },
    },
  },
});

実行すると”Invalid reference token”となります。


Uncaught (in promise) Error: Invalid reference token: tokens

上記のsignInResponseTokenPointerを設定した場合には下記のような形式でTokenを戻す必要があります。


return {
  tokens: {
    accessToken,
  },
};

/api/auth/loginから戻されたTokenは@sidebase/next-auth/dist/runtime/composables/local/useAuth.mjsファイルの下記の行で取り出しています。jsonPointerGetの中でresponseとして戻されたTokenの形式がsignInResponseTokenPointerの設定と一致するかチェックしてTokenを取り出しています。


const extractedToken = jsonPointerGet(response, config.token.signInResponseTokenPointer);

再度、ログイン画面からusernameとpasswordを入力して”sign in”ボタンをクリックするとブラウザのConsoleには/api/auth/sessionのNot Foundエラーが発生します。


GET http://localhost:3000/api/auth/session 404 (Page not found: /api/auth/session)

開発サーバを起動したターミナルにはWarningが表示されます。


[Vue Router warn]: No match found for location with path "/api/auth/session"

エラーが表示される原因はsignIn関数の中で/api/auth/loginにPOSTリクエストを送信してTokenを取得した後、getSession関数が実行され/api/auth/sessionに対してGETリクエストを送信するためです。

getSession関数のコードは@sidebase/next-auth/dist/runtime/composables/local/useAuth.mjsに記述されています。モジュールオプションのgetSessionのエンドポイントのデフォルト値はsessionなのでHeaderに/api/auth/loginから取得したTokenを設定して/api/auth/sessionに対してGETリクエストを送信しています。取得したTokenの検証を/api/auth/sessionのエンドポイントで行います。


const getSession = async (getSessionOptions) => {
  const nuxt = useNuxtApp();
  const config = useTypedBackendConfig(useRuntimeConfig(), "local");
  const { path, method } = config.endpoints.getSession;
  const { data, loading, lastRefreshedAt, token, rawToken } = useAuthState();
  if (!token.value && !getSessionOptions?.force) {
    return;
  }
  const headers = new Headers(token.value ? { [config.token.headerName]: token.value } : void 0);
  loading.value = true;
  try {
    data.value = await _fetch(nuxt, path, { method, headers });
  } catch {
    data.value = null;
    rawToken.value = null;
  }
//略

上記のコードを見る通り/api/auth/sessionから戻される値はdata(=Session Data)に保存されます。/api/auth/sessionで行われる処理についても各自が作成する必要があります。

sessionエンドポイントの作成

server/api/authディレクトリにsession.tsファイルを作成します。


import { H3Event } from 'h3';
import jsonwebtoken from 'jsonwebtoken';
import { SECRET } from './login';

const { verify } = jsonwebtoken;

const TOKEN_TYPE = 'Bearer';

const extractToken = (authHeaderValue: string) => {
  const [, token] = authHeaderValue.split(`${TOKEN_TYPE} `);
  return token;
};

const ensureAuth = (event: H3Event) => {
  const authHeaderValue = getRequestHeader(event, 'authorization');
  if (typeof authHeaderValue === 'undefined') {
    throw createError({
      statusCode: 403,
      statusMessage:
        'Need to pass valid Bearer-authorization header to access this endpoint',
    });
  }

  const extractedToken = extractToken(authHeaderValue);
  try {
    return verify(extractedToken, SECRET);
  } catch (error) {
    console.error("Login failed. Here's the raw error:", error);
    throw createError({
      statusCode: 403,
      statusMessage: 'You must be logged in to use this endpoint',
    });
  }
};

export default eventHandler((event) => {
  const data = ensureAuth(event);
  return data;
});

session.get.tsファイルの中では送信されてきたGETリクエストのHeaderからTokenを取り出してjsonwebtokenを利用してTokenが有効かどうかverify関数で検証を行っています。合わせてverify関数でTokenに含まれているデータを取り出して戻しています。session.get.tsファイルではTokenの検証とTokenに含まれるデータを戻す処理が必要となります。

session.get.tsファイルを作成後はブラウザのデベロッパーツールのネットワークタブを確認すると/auth/api/sessionへのGETリクエストが正常に動作しており、Tokenに保存されていたデータが戻されていることが確認できます。


{username: "John", iat: 1703383550, exp: 1703383565}

しかしコンソールには以下のエラーメッセージが表示されます。


Uncaught (in promise) Error: Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.

ログインした後にデフォルトでは”http://localhost:3000/login”にリダイレクトされるため上記のエラーが発生しています。

メッセージに表示されているようにエラーを解消するためにはlogin.vueファイルのsignInメソッドのsignInオプションでexternalプロパティの値をtrueに設定します。このオプションは別のサイトにリダイレクトしたい場合に設定するプロパティです。


<form @submit.prevent="signIn({ username, password }, { external: true })">

設定後はエラーは表示されなくなります。

signInオプションにはexternalの他にもredirectとcallbackUrlがあり、callbackUrlを設定することでサインイン完了後にリダイレクトさせたいページを指定することができます。


<form
  @submit.prevent="
    signIn({ username, password }, { callbackUrl: 'dashboard' })
  "
>

redirectはデフォルトではtrueなのでリダイレクトさせたくない場合にfalseを設定することができます。

設定後はログインが完了すると/dashboardページにリダイレクトされます。リダイレクト後に”/”にアクセスすると”authenticated”の文字列を確認することができます。ここでようやくログインによる認証処理が完了しました。

ログイン後のステータスの確認
ログイン後のステータスの確認

unauthenticatedOnlyの設定

ログイン後にはLoginページにアクセスする必要がないためログインが行われていない場合のみアクセスを行えるように設定(unauthenticatedOnly)します。もしログインしている場合には指定したページにリダイレクトの設定(navigateAuthenticatedTo)を行います。


<script setup>
definePageMeta({
  auth: {
    unauthenticatedOnly: true,
    navigateAuthenticatedTo: "/dashboard",
  },
});
import { ref } from "vue";
//略

設定後はログイン後にloginページにアクセスするとdashboardページにリダイレクトされます。ログイン前はそのままloginページにアクセスできます。

ログアウト処理

ログアウト処理はuseAuth Composableから戻されるsignOut関数を実行します。layoutsディレクトリのdefault.vueファイルのリンクメニューにlogoutボタンを追加します。


<script setup>
const { signOut } = useAuth();
</script>
<template>
  <div>
    <nav>
      <ul>
        <li><NuxtLink to="/">Home</NuxtLink></li>
        <li><NuxtLink to="/login">Login</NuxtLink></li>
        <li><NuxtLink to="/dashboard">Dashboard</NuxtLink></li>
        <li><button @click="signOut">Logout</button></li>
      </ul>
    </nav>
    <slot />
  </div>
</template>

ログイン画面で認証が完了するとDashboardページに移動しますが認証のステータスを確認するために”/”ページに移動してStatusがauthenticatedであることを確認してLogoutボタンをクリックしてください。ログアウト処理が行われるためStatusからauthenticatedからunauthenticatedに代わります。

ログアウトは完了してもブラウザのコンソールには/api/auth/logoutページのNot Foundエラーが表示されます。


FetchError: [POST] "http://localhost:3000/api/auth/logout": 404 Page not found: /api/auth/logout

logoutエンドポイントの作成

login, sessionエンドポイントの場合とは異なり、logoutエンドポイントではTokenを戻すことやdataを戻すなど決められた処理を記述する必要はないため自由に設定を行うことができます。logout後に実行したい処理を記述することになります。

/server/api/authディレクトリにlogout.tsファイルを作成して以下のコードを記述します。ただメッセージを戻しているだけです。


export default eventHandler(() => ({ messsage: "logout successfully" }));

デベロッパーツールのネットワークタブを見ると/api/auth/logoutへのPOSTリクエストには成功してmessageが戻されていますがloginの場合と同様に下記のエラーメッセージがコンソールに表示されます。


Uncaught (in promise) Error: Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.

signOutの引数にもsignInと同じcallbackUrl, redirect, externalオプションを設定することができるのでcallbackUrlを設定します。


<button @click="() => signOut({ callbackUrl: '/' })">Logout</button>

callbackUrlを設定後はエラーメッセージの表示は消え、ログイン後にLogoutボタンをクリックすると”/”にリダイレクトされます。

Logoutボタンについてはstatusの値で表示・非表示が自動で切り替わるようにv-showを設定することもできます。


<script setup>
const { signOut, status } = useAuth();
</script>
<template>
  <div>
    <nav>
      <ul>
        <li><NuxtLink to="/">Home</NuxtLink></li>
        <li><NuxtLink to="/login">Login</NuxtLink></li>
        <li><NuxtLink to="/dashboard">Dashboard</NuxtLink></li>
        <li v-show="status === 'authenticated'">
          <button @click="() => signOut({ callbackUrl: '/' })">Logout</button>
        </li>
      </ul>
    </nav>
    <slot />
  </div>
</template>

ここまでの設定で@sidebase/nuxt-authのlocal Providerでの認証設定は完了です。

その他の機能

Cookieについて

ログインによる認証が完了するとauth:tokenという名前で値にはTokenが設定されたCookieが作成されます。ブラウザのデベロッパーツールのアプリケーションのストレージでCookieの情報を確認することができます。

Cookieの確認
Cookieの確認

Cookieが保存されている場合はブラウザを閉じて再度ページを開いても引き続き認証済みとして接続を行うことができます。Cookieの有効期限もモジュールオプションのmaxAgeInSecondsで設定することができますがCookieが有効期間以内でもTokenの有効期限が短くTokenの有効期限が過ぎればstatusはunauthenticatedとなります。

Guest Mode

Guest Modeという機能がありますがこの機能はログインしていない時だけにアクセスできるページを設定する場合に利用します。本文書でもすでに利用しておりloginページでGuest Modeが利用されています。


definePageMeta({
  auth: {
    unauthenticatedOnly: true,
    navigateAuthenticatedTo: '/',
  },
})

middlewareの@sidebar/nuxt-auth/dist/runtime/middleware/auth.mjsファイルの中でもisGuetModeという変数を利用してGuest Modeの場合の処理についての記述があります。Guest Modeで認証されていない場合はそのままページにアクセスができ、Guest Modeで認証されている場合はnavigateAuthenticatedToにリダイレクトされます。


//略
const isGuestMode = typeof metaAuth === "object" && metaAuth.unauthenticatedOnly;
if (isGuestMode && status.value === "unauthenticated") {
  return;
}
if (typeof metaAuth === "object" && !metaAuth.unauthenticatedOnly) {
  return;
}
if (status.value === "authenticated") {
  if (isGuestMode) {
    return navigateTo(metaAuth.navigateAuthenticatedTo ?? "/");
  }
  return;
}
//略

getSessionについて

Tokenの有効期限が切れているかどうかのチェックはgetSessionで実行される/api/auth/sessionのTokenの検証で行われます。ページ間の移動で行われるMiddlewareではTokenの検証は行われず、 statusの値がauthenticatedかどうかでページにアクセスできるかどうかが決まります。

middlewareの@sidebar/nuxt-auth/dist/runtime/middleware/auth.mjsの処理の一部です。


//略
const { status, signIn } = useAuth();
//略
if (status.value === "authenticated") {
  if (isGuestMode) {
    return navigateTo(metaAuth.navigateAuthenticatedTo ?? "/");
  }
  return;
}
//略

enableRefreshOnWindowFocus

enableRefreshOnWindowFocusはブラウザから一度は離れ、そのページが再度フォーカスされたらgetSession関数を実行してTokenの検証とデータを取得する機能です。

動作確認の方法は簡単でブラウザで複数のタブを開きます。ブラウザの別のタブに移動して戻ってきた時に/api/auth/sessionにGETリクエストが送信されます。リクエストはブラウザのデベロッパーツールのネットワークタブで確認できます。Tokenが有効でなくなかった場合はstatusはunauthenticatedになります。

@sidebase/nuxt-auth/dist/runtime/plugin.mjsファイルの中で設定が行われており”visibilitychange”イベントをイベントリスナーで登録、監視しており、visibilitychangeがあるとgetSession関数が実行されます。


const visibilityHandler = () => {
  if (enableRefreshOnWindowFocus && document.visibilityState === "visible") {
    getSession();
  }
};
//略
document.addEventListener("visibilitychange", visibilityHandler, false);

enableRefreshPeriodically

enableRefreshPeriodicallyはデフォルトではfalseになってきますが、定期的にgetSession関数が実行されTokenの検証とデータの取得を行います。値はboolean値と数値をとることができ、数値を設定した場合はmillisecondとして設定されます。3000を設定すると3秒ごとにに/api/auth/sessoinへGETリクエストが送信されます。trueに設定した場合は1秒ごとに/api/auth/sessionへGETリクエストが送信されます。


// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ["@sidebase/nuxt-auth"],
  auth: {
    globalAppMiddleware: true,
    provider: {
      type: "local",
      // token: {
      //   signInResponseTokenPointer: "/tokens/accessToken",
      // },
    },
    session: {
      enableRefreshPeriodically: 1000,
    },
  },
});

@sidebase/nuxt-auth/dist/runtime/plugin.mjsファイルの中で設定が行われておりdataが存在する場合にsetIntervalを利用して指定した間隔でgetSessionを実行しています。


if (enableRefreshPeriodically !== false) {
  const intervalTime = enableRefreshPeriodically === true ? 1e3 : enableRefreshPeriodically;
  refetchIntervalTimer = setInterval(() => {
    if (data.value) {
      getSession();
    }
  }, intervalTime);
}

ここまで読み進めていただければ@sidebase/nuxt-authのlocal Providerではどのような機能を持ち、どのようなコードで処理が行われているか理解も深まったかと思います。ぜひ要件にあえば活用してみてください。