Nuxt3での簡易の認証フローを構築する

code on a screen NuxtJS
Photo by Markus Spiske on Pexels.com
この記事は約8分で読めます。

Nuxt3でアクセストークンを使った簡易な認証フローの構築をしてみます。

簡易な認証フローでは、route middlewareを使います。route middlewareはNuxt2にも同様にあった仕組みです。サーバーサイド(SSRサーバ初回アクセス時)、クライアントサイドで動作し、ページ遷移時に認証などのフィルタを挟んだりすることができます。ページコンポーネントに以下のように記述することで、ミドルウェアを宣言的に適用することができます。

// Nuxt2
// ページやレイアウトコンポーネントで指定
export default {
  middleware: ['auth']
}

// Nuxt3
// ページコンポーネントで指定
<script setup>
definePageMeta({
  middleware: ["auth"]
  // or middleware: 'auth'
})
</script>

参照:ミドルウェアディレクトリ 

なお、server/middlewareという仕組みもありますが、これはサーバーリクエストごとに動作するミドルウェアです。ドキュメントにもあるように、何らかのチェックやヘッダの操作、ロギングなどに使用する用途のようです。expressでrouteをマウントしても呼ばれます。Nuxt3の内部で使われているh3 httpフレームワークのミドルウェアとして登録されます。

https://v3.nuxtjs.org/guide/features/server-routes#server-middleware

そのため今回は、route middlewareの仕組みを使って構築してみます。

テスト環境

  • nuxt@3.0.0-rc.8

今回のフロー

以下のような構成にしてみます。nuxt-auth moduleもcookieを使った同様の機構があります。

  • トークンはcookieに保存、ログイン時にcookieに書き込む。クライアント側でもread/writeするのでHttpOnly属性はなし。
  • SSR時は、cookieからトークンを読み出して使う。
  • route middlewareでは、cookieから値を読み出しログイン済みかどうかをチェックする。

この他、以下の仕組みを使うので補足します。

route middleware

route middlewareですが、middlewareディレクトリに配置すると自動でインポートされます。.globalというサフィックスをつけることで、全体に対して適用することができます。

  • middleware/auth.ts
    • definePageMetaでページコンポーネントに対してミドルウェアを適用する
  • middleware/auth.global.ts
    • 全体に渡ってミドルウェアが適用される

useCookie

useCookieというcomposableライブラリを使用します。useCookieは、サーバサイドとクライアントサイドで使えます。useCookieによる返り値に対しvalueを書き込みすることで、裏でNuxtが上手いこと処理をしてくれます(具体的には後述の通り)。

// cookieはRefオブジェクト
const cookie = useCookie<string | null>('access_token', cookieOptions)

// クライアントサイド
//  document.cookieに書き込みされる
//  具体的にはcookieはwatch apiの監視対象となっているので値の更新を検知して書き込まれます
// サーバサイド
//  event.req.headers.cookieに書き込みされる
//  app:rendered、app:redirectedフック実行のタイミングでhttpヘッダに書き込まれる
cookie.value = accessToken

これにより、SSR時でもアクセストークンを取得し、フェッチリクエスト時にヘッダにトークンを埋め込むことができるようにします。

クライアントサイドの動作

クライアントサイドでは、document.cookieより指定したcookieを取得します。そのため、HttpOnlyが指定されたcookieは使えません。useCookieにより得たcookieはrefオブジェクトとなっており、valueに値を書き込むことでdocument.cookieにシンクされます。

サーバサイドの動作

サーバサイドでは、フックのタイミングでレスポンスヘッダにcookieが書き込まれます。

route middlewareを使った例

では、実際に構築してみます。フローを確認するためのサンプルなので、エラーチェックやその他バリデーションなど厳格さは欠いていますことご容赦ください。

server/api/login.ts

今回は、トークンを返すだけの簡易な処理としています。

export default defineEventHandler((event) => {
  return {
    accessToken: "dummy token",
  };
});

server/api/posts.ts

ログイン後に何らかの投稿記事一覧を返す処理とします。リクエストヘッダから、Authorizationヘッダを読みトークンを確認します。トークンがあれば、ひとまず記事一覧を返すとします。

import { getRequestHeader } from "h3";
import consola from "consola";

export default defineEventHandler((event) => {
  const token = getRequestHeader(event, "Authorization");

  consola.info(token: ${token});

  // サンプルとしてtokenの有無だけチェック
  if (!token) {
    throw createError({
      statusCode: 401,
      statusMessage: "Not authorized",
    });
  }

  // TODO: validate(token)

  return {
    posts: [
      { title: "title1", content: "test content", author: "tarou" },
      { title: "title2", content: "test content2", author: "tarou" },
      { title: "title3", content: "test content3", author: "tarou" },
    ],
  };
});

middleware/auth.global.ts

useCookieを使っています。ログインに成功すると、cookieに値を書き込みます。同時にdocument.cookieにも書き込まれます。

import type { Ref } from "vue";

export const useAuth = () => {
  const cookie = useCookie("access_token");
  // サンプルではcookieの存在有無だけ
  // TODO: validate
  const initialValue = cookie.value ? true : false;

  const loggedIn = useState("loggedIn", () => initialValue);

  const login = (loggedIn: Ref<boolean>) => async () => {
    const data = await $fetch("/api/login");
    cookie.value = data.accessToken;
    loggedIn.value = true;
    return true;
  };

  const logout = (loggedIn: Ref<boolean>) => async () => {
    loggedIn.value = false;
    cookie.value = null;
  };

  const getToken = () => {
    return cookie.value;
  };

  return {
    loggedIn,
    getToken,
    login: login(loggedIn),
    logout: logout(loggedIn),
  };
};

pages/login.vue

<script setup>
const login = () => {
  const { login } = useAuth();
  login().then(() => {
    const router = useRouter();
    router.replace("/dashboard");
  });
};
</script>

<template>
  <div>
    <button @click="login">Login</button>
  </div>
</template>

pages/dashboard.vue

<script setup lang="ts">
const { getToken } = useAuth(); // cookieから読み出し
const { data } = await useFetch("/api/posts", {
  baseURL: "http://localhost:3000",
  headers: {
    Authorization: Bearer ${getToken()},
  },
});

const logout = () => {
  const { logout } = useAuth();
  const router = useRouter();
  logout();
  router.replace("/login");
};
</script>

<template>
  <div>
    <template v-if="data?.posts">
      <ul v-for="(post, i) in data.posts" :key="i">
        <li>{{ post.title }}</li>
      </ul>
    </template>

    <div>
      <button @click="logout">logout</button>
    </div>
  </div>
</template>

以下のような感じで動きます。

 参考リンク

コメント

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