Nuxt Auth Utils
安全でシールドされたクッキーセッションを使用して、Nuxtアプリケーションに認証を追加します。
機能
- ハイブリッドレンダリングサポート (SSR / CSR / SWR / プリレンダリング)
- 20以上のOAuthプロバイダー
- パスワードハッシング
- WebAuthn(パスキー)
useUserSession()
Vueコンポーザブル- ツリーシェイク可能なサーバーユーティリティ
<AuthState>
コンポーネント- フックによる拡張が可能
UnJSからの依存関係はわずかであり、複数のJS環境(Node、Deno、Workers)で動作し、TypeScriptで完全に型付けされています。
要件
このモジュールは、サーバーAPIルート(nuxt build
)を使用するため、Nuxtサーバーで実行されている場合のみ動作します。
つまり、nuxt generate
ではこのモジュールを使用できません。
ただし、ハイブリッドレンダリングを使用してアプリケーションのページをプリレンダリングするか、サーバーサイドレンダリングを完全に無効にすることができます。
クイックセットアップ
- Nuxtプロジェクトに
nuxt-auth-utils
を追加します。
npx nuxi@latest module add auth-utils
.env
ファイルに、少なくとも32文字のNUXT_SESSION_PASSWORD
環境変数を追加します。
# .env
NUXT_SESSION_PASSWORD=password-with-at-least-32-characters
NUXT_SESSION_PASSWORD
が設定されていない場合、Nuxtを開発モードで初めて実行すると、Nuxt Auth Utilsによって自動的に生成されます。
- これで完了です!Nuxtアプリに認証を追加できます ✨
Vueコンポーザブル
Nuxt Auth Utilsは、現在のユーザーセッションを取得するためのプラグインを自動的に追加し、Vueコンポーネントからアクセスできるようにします。
ユーザーセッション
<script setup>
const { loggedIn, user, session, fetch, clear } = useUserSession()
</script>
<template>
<div v-if="loggedIn">
<h1>Welcome {{ user.login }}!</h1>
<p>Logged in since {{ session.loggedInAt }}</p>
<button @click="clear">Logout</button>
</div>
<div v-else>
<h1>Not logged in</h1>
<a href="/auth/github">Login with GitHub</a>
</div>
</template>
TypeScriptシグネチャ
interface UserSessionComposable {
/**
* Computed indicating if the auth session is ready
*/
ready: ComputedRef<boolean>
/**
* Computed indicating if the user is logged in.
*/
loggedIn: ComputedRef<boolean>
/**
* The user object if logged in, null otherwise.
*/
user: ComputedRef<User | null>
/**
* The session object.
*/
session: Ref<UserSession>
/**
* Fetch the user session from the server.
*/
fetch: () => Promise<void>
/**
* Clear the user session and remove the session cookie.
*/
clear: () => Promise<void>
}
!重要 Nuxt Auth Utilsはセッション管理に
/api/_auth/session
ルートを使用します。APIルートミドルウェアがこのパスと干渉しないようにしてください。
サーバーユーティリティ
次のヘルパーは、server/
ディレクトリに自動的にインポートされます。
セッション管理
// Set a user session, note that this data is encrypted in the cookie but can be decrypted with an API call
// Only store the data that allow you to recognize a user, but do not store sensitive data
// Merges new data with existing data using unjs/defu library
await setUserSession(event, {
// User data
user: {
login: 'atinux'
},
// Private data accessible only on server/ routes
secure: {
apiToken: '1234567890'
},
// Any extra fields for the session data
loggedInAt: new Date()
})
// Replace a user session. Same behaviour as setUserSession, except it does not merge data with existing data
await replaceUserSession(event, data)
// Get the current user session
const session = await getUserSession(event)
// Clear the current user session
await clearUserSession(event)
// Require a user session (send back 401 if no `user` key in session)
const session = await requireUserSession(event)
プロジェクトに型定義ファイル(例:auth.d.ts
)を作成してUserSession
型を拡張することにより、ユーザーセッションの型を定義できます。
// auth.d.ts
declare module '#auth-utils' {
interface User {
// Add your own fields
}
interface UserSession {
// Add your own fields
}
interface SecureSessionData {
// Add your own fields
}
}
export {}
!重要 セッションデータを暗号化してクッキーに保存するため、4096バイトのクッキーサイズ制限があります。重要な情報のみを保存してください。
OAuthイベントハンドラー
すべてのハンドラーは自動的にインポートでき、サーバールートまたはAPIルートで使用できます。
パターンはdefineOAuth<Provider>EventHandler({ onSuccess, config?, onError? })
です。例:defineOAuthGitHubEventHandler
。
このヘルパーは、プロバイダーの承認ページに自動的にリダイレクトし、結果に応じてonSuccess
またはonError
を呼び出すイベントハンドラーを返します。
config
は、nuxt.config.ts
のruntimeConfig
から直接定義できます。
export default defineNuxtConfig({
runtimeConfig: {
oauth: {
// provider in lowercase (github, google, etc.)
<provider>: {
clientId: '...',
clientSecret: '...'
}
}
}
})
環境変数を使用して設定することもできます。
NUXT_OAUTH_<PROVIDER>_CLIENT_ID
NUXT_OAUTH_<PROVIDER>_CLIENT_SECRET
プロバイダーは大文字(GITHUB、GOOGLEなど)です。
サポートされているOAuthプロバイダー
- Auth0
- AWS Cognito
- Battle.net
- Discord
- Dropbox
- GitHub
- GitLab
- Keycloak
- Linear
- Microsoft
- PayPal
- Polar
- Spotify
- Steam
- TikTok
- Twitch
- VK
- X (Twitter)
- XSUAA
- Yandex
src/runtime/server/lib/oauth/に新しいファイルを作成することで、お気に入りのプロバイダーを追加できます。
例
例:~/server/routes/auth/github.get.ts
export default defineOAuthGitHubEventHandler({
config: {
emailRequired: true
},
async onSuccess(event, { user, tokens }) {
await setUserSession(event, {
user: {
githubId: user.id
}
})
return sendRedirect(event, '/')
},
// Optional, will return a json error and 401 status code by default
onError(event, error) {
console.error('GitHub OAuth error:', error)
return sendRedirect(event, '/')
},
})
OAuthアプリの設定でコールバックURLを<your-domain>/auth/github
として設定してください。
本番環境でリダイレクトURLが一致しない場合、モジュールは正しいリダイレクトURLを推測できません。NUXT_OAUTH_<PROVIDER>_REDIRECT_URL
環境変数を設定して、デフォルトのURLを上書きできます。
パスワードハッシング
Nuxt Auth Utilsは、多くのJSランタイムでサポートされているscryptを使用してパスワードをハッシュおよび検証するためのhashPassword
とverifyPassword
などのパスワードハッシングユーティリティを提供します。
const hashedPassword = await hashPassword('user_password')
if (await verifyPassword(hashedPassword, 'user_password')) {
// Password is valid
}
nuxt.config.ts
でscryptオプションを設定できます。
export default defineNuxtConfig({
modules: ['nuxt-auth-utils'],
auth: {
hash: {
scrypt: {
// See https://github.com/adonisjs/hash/blob/94637029cd526783ac0a763ec581306d98db2036/src/types.ts#L144
}
}
}
})
WebAuthn(パスキー)
WebAuthn(Web認証)は、公開鍵暗号化を使用してパスワードをパスキーに置き換えることでセキュリティを強化するWeb標準です。ユーザーは、生体認証データ(指紋や顔認識など)または物理デバイス(USBキーなど)を使用して認証できるため、フィッシングやパスワード侵害のリスクを軽減できます。このアプローチは、主要なブラウザとプラットフォームでサポートされており、より安全でユーザーフレンドリーな認証方法を提供します。
WebAuthnを有効にするには、以下の手順が必要です。
- ピア依存関係をインストールします。
npx nypm i @simplewebauthn/server@11 @simplewebauthn/browser@11
nuxt.config.ts
で有効にします。
export default defineNuxtConfig({
auth: {
webAuthn: true
}
})
例
この例では、資格情報を登録および認証するための非常に基本的な手順を実装します。
完全なコードはplaygroundにあります。この例では、次の最小限のテーブルを持つSQLiteデータベースを使用しています。
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS credentials (
userId INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
id TEXT UNIQUE NOT NULL,
publicKey TEXT NOT NULL,
counter INTEGER NOT NULL,
backedUp INTEGER NOT NULL,
transports TEXT NOT NULL,
PRIMARY KEY ("userId", "id")
);
users
テーブルには、ユーザー名やメールアドレスなどの固有の識別子(ここではメールアドレスを使用)が必要です。新しい資格情報を生成する際には、この識別子が必須であり、ユーザーのデバイス、パスワードマネージャー、または認証装置にパスキーと共に保存されます。credentials
テーブルには、以下が保存されます。users
テーブルからのuserId
。- 資格情報の
id
(一意のインデックスとして)。 - 資格情報の
publicKey
。 counter
。資格情報が使用されるたびに、カウンターが増加します。この値を使用して、追加のセキュリティチェックを実行できます。counter
の詳細については、こちらを参照してください。この例では、カウンターは使用しません。ただし、新しい値でデータベース内のカウンターを更新する必要があります。backedUp
フラグ。通常、資格情報は生成デバイスに保存されます。パスワードマネージャーや認証装置を使用する場合、資格情報は複数のデバイスで使用できるため「バックアップ」されます。このセクションで詳細を確認してください。- 資格情報の
transports
。クライアントとの資格情報の通信方法を示す文字列の配列です。ユーザーが資格情報を使用するための正しいUIを表示するために使用されます。このセクションで詳細を確認してください。
次のコードには実際のデータベースクエリは含まれていませんが、実行する一般的な手順を示しています。完全な例はplaygroundにあります:登録、認証、およびデータベース設定。
// server/api/webauthn/register.post.ts
import { z } from 'zod'
export default defineWebAuthnRegisterEventHandler({
// optional
validateUser: z.object({
// we want the userName to be a valid email
userName: z.string().email()
}).parse,
async onSuccess(event, { credential, user }) {
// The credential creation has been successful
// We need to create a user if it does not exist
const db = useDatabase()
// Get the user from the database
let dbUser = await db.sql`...`
if (!dbUser) {
// Store new user in database & its credentials
dbUser = await db.sql`...`
}
// we now need to store the credential in our database and link it to the user
await db.sql`...`
// Set the user session
await setUserSession(event, {
user: {
id: dbUser.id
},
loggedInAt: Date.now(),
})
},
})
// server/api/webauthn/authenticate.post.ts
export default defineWebAuthnAuthenticateEventHandler({
// Optionally, we can prefetch the credentials if the user gives their userName during login
async allowCredentials(event, userName) {
const credentials = await useDatabase().sql`...`
// If no credentials are found, the authentication cannot be completed
if (!credentials.length)
throw createError({ statusCode: 400, message: 'User not found' })
// If user is found, only allow credentials that are registered
// The browser will automatically try to use the credential that it knows about
// Skipping the step for the user to select a credential for a better user experience
return credentials
// example: [{ id: '...' }]
},
async getCredential(event, credentialId) {
// Look for the credential in our database
const credential = await useDatabase().sql`...`
// If the credential is not found, there is no account to log in to
if (!credential)
throw createError({ statusCode: 400, message: 'Credential not found' })
return credential
},
async onSuccess(event, { credential, authenticationInfo }) {
// The credential authentication has been successful
// We can look it up in our database and get the corresponding user
const db = useDatabase()
const user = await db.sql`...`
// Update the counter in the database (authenticationInfo.newCounter)
await db.sql`...`
// Set the user session
await setUserSession(event, {
user: {
id: user.id
},
loggedInAt: Date.now(),
})
},
})
!重要 Webauthnは、リプレイ攻撃を防ぐためにチャレンジを使用します。デフォルトでは、このモジュールはこの機能を使用しません。チャレンジを使用する場合は(**強く推奨します**)、
storeChallenge
関数とgetChallenge
関数が提供されます。試行IDが作成され、各認証リクエストと共に送信されます。このIDを使用して、次の例のようにデータベースまたはKVストアにチャレンジを保存できます。
export default defineWebAuthnAuthenticateEventHandler({ async storeChallenge(event, challenge, attemptId) { // Store the challenge in a KV store or DB await useStorage().setItem(`attempt:${attemptId}`, challenge) }, async getChallenge(event, attemptId) { const challenge = await useStorage().getItem(`attempt:${attemptId}`) // Make sure to always remove the attempt because they are single use only! await useStorage().removeItem(`attempt:${attemptId}`) if (!challenge) throw createError({ statusCode: 400, message: 'Challenge expired' }) return challenge }, async onSuccess(event, { authenticator }) { // ... }, })
フロントエンドでは、次のようになります。
<script setup lang="ts">
const { register, authenticate } = useWebAuthn({
registerEndpoint: '/api/webauthn/register', // Default
authenticateEndpoint: '/api/webauthn/authenticate', // Default
})
const { fetch: fetchUserSession } = useUserSession()
const userName = ref('')
async function signUp() {
await register({ userName: userName.value })
.then(fetchUserSession) // refetch the user session
}
async function signIn() {
await authenticate(userName.value)
.then(fetchUserSession) // refetch the user session
}
</script>
<template>
<form @submit.prevent="signUp">
<input v-model="userName" placeholder="Email or username" />
<button type="submit">Sign up</button>
</form>
<form @submit.prevent="signIn">
<input v-model="userName" placeholder="Email or username" />
<button type="submit">Sign in</button>
</form>
</template>
完全な例については、WebAuthnModal.vue
を参照してください。
デモ
Drizzle ORMとNuxtHubを使用してhttps://todo-passkeys.nuxt.devで完全なデモを確認できます。
デモのソースコードはhttps://github.com/atinux/todo-passkeysで入手できます。
セッションの拡張
フックを利用して、独自のデータでセッションデータを拡張したり、ユーザーがセッションをクリアしたときにログを記録したりできます。
// server/plugins/session.ts
export default defineNitroPlugin(() => {
// Called when the session is fetched during SSR for the Vue composable (/api/_auth/session)
// Or when we call useUserSession().fetch()
sessionHooks.hook('fetch', async (session, event) => {
// extend User Session by calling your database
// or
// throw createError({ ... }) if session is invalid for example
})
// Called when we call useUserSession().clear() or clearUserSession(event)
sessionHooks.hook('clear', async (session, event) => {
// Log that user logged out
})
})
サーバーサイドレンダリング
クライアントとサーバーの両方から認証済みリクエストを行うことができます。ただし、useFetch()
を使用していない場合は、SSR中に認証済みリクエストを行うにはuseRequestFetch()
を使用する必要があります。
<script setup lang="ts">
// When using useAsyncData
const { data } = await useAsyncData('team', () => useRequestFetch()('/api/protected-endpoint'))
// useFetch will automatically use useRequestFetch during SSR
const { data } = await useFetch('/api/protected-endpoint')
</script>
Nuxtの
$fetch
に資格情報を追加するための未解決の問題があります。
ハイブリッドレンダリング
ページをプリレンダリングまたはキャッシュするためにNuxt routeRules
を使用する場合、Nuxt Auth Utilsはプリレンダリング中にユーザーセッションを取得せず、代わりにクライアント側(ハイドレーション後)で取得します。
これは、ユーザーセッションがセキュアなクッキーに保存されており、プリレンダリング中はアクセスできないためです。
つまり、プリレンダリング中にユーザーセッションに依存すべきではありません。
<AuthState>
コンポーネント
<AuthState>
コンポーネントを使用して、レンダリングモードを気にせずに、コンポーネントで認証関連のデータを安全に表示できます。
ヘッダーのログインボタンが一般的なユースケースです。
<template>
<header>
<AuthState v-slot="{ loggedIn, clear }">
<button v-if="loggedIn" @click="clear">Logout</button>
<NuxtLink v-else to="/login">Login</NuxtLink>
</AuthState>
</header>
</template>
ページがキャッシュまたはプリレンダリングされている場合、クライアント側でユーザーセッションが取得されるまで何もレンダリングされません。
placeholder
スロットを使用して、サーバー側と、プリレンダリングされたページのユーザーセッションがクライアント側で取得されている間にプレースホルダーを表示できます。
<template>
<header>
<AuthState>
<template #default="{ loggedIn, clear }">
<button v-if="loggedIn" @click="clear">Logout</button>
<NuxtLink v-else to="/login">Login</NuxtLink>
</template>
<template #placeholder>
<button disabled>Loading...</button>
</template>
</AuthState>
</header>
</template>
routeRules
を使用してルートをキャッシュする場合は、ユーザーセッションのクライアント側取得をサポートするために、Nitro >= 2.9.7
を使用してください。
設定
h3 useSession
にデフォルトオプションを提供するために、runtimeConfig.session
を使用しています。
nuxt.config.ts
でオプションを上書きできます。
export default defineNuxtConfig({
modules: ['nuxt-auth-utils'],
runtimeConfig: {
session: {
maxAge: 60 * 60 * 24 * 7 // 1 week
}
}
})
デフォルトは次のとおりです。
{
name: 'nuxt-session',
password: process.env.NUXT_SESSION_PASSWORD || '',
cookie: {
sameSite: 'lax'
}
}
setUserSession
関数とreplaceUserSession
関数の第3引数として渡すことで、セッション設定を上書きすることもできます。
await setUserSession(event, { ... } , {
maxAge: 60 * 60 * 24 * 7 // 1 week
})
すべてのオプションについては、SessionConfig
を参照してください。
その他
- nuxt-authorization:Nuxtアプリ内で権限を管理するための承認モジュールで、
nuxt-auth-utils
と互換性があります。
開発
# 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