Nuxt Nation カンファレンス開催! 11月12日〜13日

nuxt-authorization

アプリとサーバー内部の権限を管理します。

Nuxt Authorization

npm versionnpm downloadsLicenseNuxt

NuxtとNitroの両方で簡単に認証処理を行います。

このモジュールはACLやRBACを実装していません。独自の認証ロジックを実装するために使用できる低レベルのプリミティブを提供します。

!注記 将来的には、このモジュールはNitroモジュールとNuxtモジュールとして利用可能になる可能性がありますが、Nitroモジュールはまだ準備ができていません。

このモジュールとそれが解決する問題の詳細については、Nuxtにおける認証に関するブログ記事をご覧ください。

機能

  • ⛰  クライアント(Nuxt)とサーバー(Nitro)の両方で動作します
  • 🌟  権限を一度記述して、どこでも使用できます
  • 👨‍👩‍👧‍👦  認証レイヤーに依存しません
  • 🫸  コンポーネントを使用して、UIの一部を条件付きで表示できます
  • 💧  プリミティブにアクセスして、完全にカスタマイズできます

クイックセットアップ

1つのコマンドでNuxtアプリケーションにモジュールをインストールします

npx nuxi module add nuxt-authorization

これで完了です!Nuxtアプリでモジュールを使用できます ✨

ドキュメント

!注記 プレイグラウンドでモジュールの動作を確認できます。

設定

モジュールを使用し、最初の権限を定義する前に、2つのリゾルバーを提供する必要があります。これらの関数は内部的にユーザーの取得に使用されますが、実装する必要があります。これにより、モジュールは認証レイヤーに依存しなくなります。

Nuxtアプリの場合、plugins/authorization-resolver.tsに新しいプラグインを作成します。

export default defineNuxtPlugin({
  name: 'authorization-resolver',
  parallel: true,
  setup() {
    return {
      provide: {
        authorization: {
          resolveClientUser: () => {
            // Your logic to retrieve the user from the client
          },
        },
      },
    }
  },
})

この関数は、クライアントで認証を確認するたびに呼び出されます。ユーザーオブジェクトを返すか、ユーザーが認証されていない場合はnullを返す必要があります。非同期にすることができます。

Nitroサーバーの場合、server/plugins/authorization-resolver.tsに新しいプラグインを作成します。

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    event.context.$authorization = {
      resolveServerUser: () => {
        // Your logic to retrieve the user from the server
      },
    }
  })
})

!注記 event.contextの詳細については、こちらをご覧ください。

このリゾルバーはrequestフック内で設定され、イベントを受け取ります。これを使用して、セッションまたはリクエストからユーザーを取得できます。ユーザーオブジェクトを返すか、ユーザーが認証されていない場合はnullを返す必要があります。非同期にすることができます。

一般的に、アプリの起動時にユーザーを取得し、それを保存するためにプラグインを使用します。リゾルバー関数は、保存されたユーザーのみを返し、再度フェッチするべきではありません(そうでない場合、深刻なパフォーマンスの問題が発生する可能性があります)。

nuxt-auth-utilsを使用した例

モジュールnuxt-auth-utilsは、Nuxtの認証レイヤーを提供します。このモジュールを使用する場合は、次のリゾルバーを使用できます。

Nuxtプラグイン

export default defineNuxtPlugin({
  name: 'authorization-resolver',
  parallel: true,
  setup() {
    return {
      provide: {
        authorization: {
          resolveClientUser: () => useUserSession().user.value,
        },
      },
    }
  },
})

Nitroプラグイン

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    event.context.$authorization = {
      resolveServerUser: async () => {
        const session = await getUserSession(event)
        return session.user ?? null
      },
    }
  })
})

簡単!

権限の定義

!注記 今後のNuxt4では、権限をapp/utils/abilities.tsファイルに保存できます。その後、~/utils/abilitiesを使用してサーバーフォルダーにインポートできます。プロジェクトOrionの例を参照してください。

!注記 Nuxt4では、クライアントとサーバー間でコードを簡単に共有するための新しいsharedディレクトリが導入されます。このissueを参照してください。

これでリゾルバーが設定されたので、最初の権限を定義できます。権限とは、少なくともユーザーを受け取り、ユーザーがアクションを実行できるかどうかを示すブール値を返す関数です。追加の引数も受け取ることができます。

権限を作成するには、新しいファイルutils/abilities.tsを作成することをお勧めします。

export const listPosts = defineAbility(() => true) // Only authenticated users can list posts

export const editPost = defineAbility((user: User, post: Post) => {
  return user.id === post.authorId
})

多くの権限がある場合は、utils/abilities/ディレクトリを作成し、各権限ごとにファイルを作成することをお勧めします。utilsディレクトリに権限を配置すると、クライアントで自動インポートが機能し、サーバーでは~/utils/abilitiesで簡単なインポートが可能になります。

デフォルトでは、ゲストはどのアクションも実行できず、権限は呼び出されません。この動作は、権限ごとに変更できます。

export const listPosts = defineAbility({ allowGuest: true }, (user: User | null) => true)

これで、認証されていないユーザーが投稿をリストできるようになりました。

権限の使用

権限を使用するには、allowsdeniesauthorizeという3つのバウンサー関数にアクセスできます。これらは両方ともクライアントとサーバーで使用できます。*実装は異なりますが、APIは同じであり、開発者にとって完全に透過的です。*

allows関数は、ユーザーがアクションを実行できるかどうかを示すブール値を返します。

if (await allows(listPosts)) {
  // User can list posts
}

denies関数は、ユーザーがアクションを実行できないかどうかを示すブール値を返します。

if (await denies(editPost, post)) {
  // User cannot edit the post
}

authorize関数は、ユーザーがアクションを実行できない場合にエラーをスローします。

await authorize(editPost, post)

// User can edit the post

権限の戻り値ごとにエラーメッセージとステータスコードをカスタマイズできます。これは、リソースの存在をユーザーに気付かせないように、403の代わりに404を返すのに役立ちます。

export const editPost = defineAbility((user: User, post: Post) => {
  if(user.id === post.authorId) {
    return true // or allow()
  }

  return deny('This post does not exist', 404)
})

allowdenytruefalseを返すことと似ていますが、denyでは、エラーのカスタムメッセージとステータスコードを返すことができます。

ほとんどの場合、APIエンドポイントはauthorizeを使用します。これは、パラメーターが不要な場合はエンドポイントの最初の行、またはユーザーがリソースにアクセスできるかどうかを確認するためのデータベースクエリの後で使用できます。H3Errorなので、Nitroサーバーによってキャッチされるため、エラーをキャッチする必要はありません。

allows関数とdenies関数は、クライアントで条件付きレンダリングやロジックを実行するのに役立ちます。これらを使用して、認証ロジックをきめ細かく制御することもできます。

コンポーネントの使用

このモジュールは、UIの一部を条件付きで表示するのに役立つ2つのコンポーネントを提供します。投稿を編集するボタンがあるとします。権限のないユーザーには、このボタンが表示されないようにする必要があります。

<template>
  <Can
    :ability="editPost"
    :args="[post]" // Optional if the ability does not take any arguments
  >
    <button>Edit</button>
  </Can>
</template>

Canコンポーネントは、ユーザーが投稿を編集できる場合にのみボタンをレンダリングします。ユーザーが投稿を編集できない場合、ボタンはレンダリングされません。

対照的に、Cannotコンポーネントを使用して、ユーザーが投稿を編集できない場合にのみボタンをレンダリングできます。

<template>
  <Cannot
    :ability="editPost"
    :args="[post]" // Optional if the ability does not take any arguments
  >
    <p>You're not allowed to edit the post.</p>
  </Cannot>
</template>

Bouncerコンポーネントは、単一のコンポーネント内でcanとcannotの両方のシナリオをより柔軟で一元化された方法で処理します。CanコンポーネントとCannotコンポーネントを個別に使用する代わりに、Bouncerコンポーネントとその名前付きスロットを利用して、統合されたブロックで両方の状態を処理できます。

<Bouncer
  :ability="editPost"
  :args="[post]" // Optional if the ability does not take any arguments
>
  <template #can>
    <button>Edit</button>
  </template>

  <template #cannot>
    <p>You're not allowed to edit the post.</p>
  </template>
</Bouncer>

貢献

ローカル開発
# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with the playground
npm run dev

# Build the playground
npm run dev:build

# Run ESLint
npm run lint

# Run Vitest
npm run test
npm run test:watch

# Release new version
npm run release

クレジット

このモジュール(コードとデザインの両方)は、Adonis Bouncerから多大な影響を受けています。これはよく書かれたパッケージであり、毎回車輪の再発明は不要だと思います。

ライセンス

MITライセンス