Nuxt Auth Utils
セキュアで密閉されたクッキーセッションでNuxtアプリケーションに認証を追加します。
機能
- ハイブリッドレンダリング対応 (SSR / CSR / SWR / プリレンダリング)
- 40以上のOAuthプロバイダー
- パスワードハッシュ化
- WebAuthn (パスキー)
useUserSession()Vueコンポーザブル- ツリーシェイク可能なサーバーユーティリティ
<AuthState>コンポーネント- フックによる拡張可能
- WebSocket対応
依存関係は少なく(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 Auth Utilsは開発モードで初めてNuxtを実行するときに自動的に1つ生成します。
- これで完了です!Nuxtアプリに認証を追加できるようになりました ✨
Vue Composable
Nuxt Auth Utilsは、現在のユーザーセッションを取得するためのいくつかのプラグインを自動的に追加し、Vueコンポーネントからアクセスできるようにします。
ユーザーセッション
<script setup>
const { loggedIn, user, session, fetch, clear, openInPopup } = 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>
<!-- or open the OAuth route in a popup -->
<button @click="openInPopup('/auth/github')">Login with GitHub</button>
</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>
/**
* Open the OAuth route in a popup that auto-closes when successful.
*/
openInPopup: (route: string, size?: { width?: number, height?: number }) => 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型を拡張することで、ユーザーセッションの型を定義できます。
!注意 Nuxt >=4.0.0 または互換バージョン4を使用している場合は、サーバーとクライアントで正しい型を取得するために、
auth.d.tsファイルをsharedディレクトリに追加してください。
// 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 {}
!重要 セッションデータを暗号化してCookieに保存するため、4096バイトのCookieサイズ制限があります。必要不可欠な情報のみを保存してください。
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_IDNUXT_OAUTH_<PROVIDER>_CLIENT_SECRET
Provider は大文字です (GITHUB, GOOGLE など)。
サポートされているOAuthプロバイダー
- Apple
- Atlassian
- Auth0
- Authentik
- AWS Cognito
- Azure B2C
- Battle.net
- Bluesky (AT Protocol)
- Discord
- Dropbox
- GitHub
- GitLab
- Gitea
- Heroku
- Hubspot
- Kick
- Keycloak
- Line
- Linear
- LiveChat
- Microsoft
- Okta
- Ory
- PayPal
- Polar
- Salesforce
- Seznam
- Slack
- Spotify
- Steam
- Strava
- TikTok
- Twitch
- VK
- WorkOS
- X (Twitter)
- XSUAA
- Yandex
- Zitadel
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環境変数を設定して、デフォルトのものを上書きできます。
パスワードハッシュ化
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
}
}
}
})
ATプロトコル
ATプロトコル(例:Bluesky)に依存するソーシャルネットワークは、通常のOAuthフローとは若干異なります。
ATプロトコルでOAuthを有効にするには、次の手順を実行する必要があります。
- ピア依存関係をインストールする
npx nypm i @atproto/oauth-client-node @atproto/api
nuxt.config.tsで有効にする
export default defineNuxtConfig({
auth: {
atproto: true
}
})
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
async validateUser(userBody, event) {
// bonus: check if the user is already authenticated to link a credential to his account
// We first check if the user is already authenticated by getting the session
// And verify that the email is the same as the one in session
const session = await getUserSession(event)
if (session.user?.email && session.user.email !== userBody.userName) {
throw createError({ statusCode: 400, message: 'Email not matching curent session' })
}
// If he registers a new account with credentials
return z.object({
// we want the userName to be a valid email
userName: z.string().email()
}).parse(userBody)
},
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はプリレンダリング中にユーザーセッションを取得せず、代わりにクライアント側(ハイドレーション後)で取得します。
これは、ユーザーセッションが安全なCookieに保存されており、プリレンダリング中にアクセスできないためです。
これは、プリレンダリング中にユーザーセッションに依存すべきではないことを意味します。
nuxt.config.tsのloadStrategyオプションで、Nuxt Auth Utilsにクライアント側でのみユーザーセッションを取得するよう指示することもできます。
export default defineNuxtConfig({
auth: {
loadStrategy: 'client-only'
}
})
client-onlyロード戦略を使用している場合でも、useUserSessionコンポーザブルからfetchを呼び出すことで、サーバー側でユーザーセッションを手動で取得できます。
<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>
ページがキャッシュされているかプリレンダリングされている場合、またはロード戦略がclient-onlyに設定されている場合、クライアント側でユーザーセッションがフェッチされるまで何もレンダリングされません。
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を使用してください。
WebSocketサポート
Nuxt Auth UtilsはNitro WebSocketsと互換性があります。
nuxt.config.tsでexperimental.websocketオプションを有効にしてください。
export default defineNuxtConfig({
nitro: {
experimental: {
websocket: true
}
}
})
upgrade関数でrequireUserSession関数を使用すると、WebSocket接続をアップグレードする前にユーザーが認証されているかどうかを確認できます。
// server/routes/ws.ts
export default defineWebSocketHandler({
async upgrade(request) {
// Make sure the user is authenticated before upgrading the WebSocket connection
await requireUserSession(request)
},
async open(peer) {
const { user } = await requireUserSession(peer)
peer.send(`Hello, ${user.name}!`)
},
message(peer, message) {
peer.send(`Echo: ${message}`)
},
})
次に、アプリケーションでuseWebSocketコンポーザブルを使用してWebSocketに接続できます。
<script setup>
const { status, data, send, open, close } = useWebSocket('/ws', { immediate: false })
// Only open the websocket after the page is hydrated (client-only)
onMounted(open)
</script>
<template>
<div>
<p>Status: {{ status }}</p>
<p>Data: {{ data }}</p>
<p>
<button @click="open">Open</button>
<button @click="close(1000, 'Closing')">Close</button>
<button @click="send('hello')">Send hello</button>
</p>
</div>
</template>
設定
runtimeConfig.sessionを活用して、h3 useSessionにデフォルトオプションを提供します。
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
pnpm install
# Generate type stubs
pnpm run dev:prepare
# Develop with the playground
pnpm run dev
# Build the playground
pnpm run dev:build
# Run ESLint
pnpm run lint
# Run Vitest
pnpm run test
pnpm run test:watch
# Release new version
pnpm run release