Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,43 @@ jobs:
- name: Run Playwright tests using Vitest with refresh enabled
run: pnpm test:e2e

test-playground-hooks:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./playground-hooks

steps:
- uses: actions/checkout@v5

- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false

- name: Use Node.js ${{ env.NODE_VER }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
cache: "pnpm"

- name: Install deps
run: pnpm i

- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps

# Check building
- run: pnpm build

- name: Run Playwright tests using Vitest with refresh disabled
run: pnpm test:e2e
env:
NUXT_AUTH_REFRESH_ENABLED: false

- name: Run Playwright tests using Vitest with refresh enabled
run: pnpm test:e2e

test-playground-authjs:
runs-on: ubuntu-latest
defaults:
Expand Down
4 changes: 4 additions & 0 deletions docs/.vitepress/routes/navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const routes: DefaultTheme.Config['nav'] = [
text: 'Local guide',
link: '/guide/local/quick-start',
},
{
text: 'Hooks guide',
link: '/guide/hooks/quick-start',
},
],
},
{
Expand Down
18 changes: 18 additions & 0 deletions docs/.vitepress/routes/sidebar/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@ export const routes: DefaultTheme.SidebarItem[] = [
}
],
},
{
text: 'Hooks Provider',
base: '/guide/hooks',
items: [
{
text: 'Quick Start',
link: '/quick-start',
},
{
text: 'Adapter',
link: '/adapter',
},
{
text: 'Examples',
link: '/examples',
}
],
},
{
text: 'Advanced',
base: '/guide/advanced',
Expand Down
129 changes: 129 additions & 0 deletions docs/guide/hooks/adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Hooks adapter

The hooks adapter gives you total control over how different authentication functions make requests, handle responses and errors.

## In short

* `createRequest` builds and returns `{ path, request }`. When `false` was returned, function execution fully stops.

* The module calls `_fetchRaw(nuxt, path, request)`.

* If an error occurs and `onError` hook was defined, the module calls it with the `Error` and request data used. In most of the functions execution will stop on error regardless if `onError` was called.

* `onResponse` determines what the module should do next:
* `false` β€” the function will stop its execution.
* This is useful when the hook itself handled redirects, cookies or state changes.
* `undefined` β€” default behaviour, the function will continue execution, handle callbacks, `getSession` calls, etc.
* Also useful if the hook handled state/redirects/cookies.
* `{ token?, refreshToken?, session? }` β€” module will set provided tokens/session in `authState` and the function will continue execution.

## In detail

A hooks provider expects the following adapter implementation for the auth endpoints:

```ts
export interface HooksAdapter {
signIn: EndpointHooks
getSession: EndpointHooks
signOut?: EndpointHooks
signUp?: EndpointHooks
refresh?: EndpointHooks
}
```

Each `EndpointHooks` has three functions: `createRequest` and `onResponse` (required), and `onError` (optional).

## `createRequest(data, authState, nuxt)`

Prepare data for the fetch call.

Must return either an object conforming to:

```ts
interface CreateRequestResult {
// Path to the endpoint
path: string
// Request: body, headers, etc.
request: NitroFetchOptions
}
```

or `false` to stop execution (no network call will be performed).

### `authState` argument

This argument gives you access to the state of the module, allowing to read or modify session data or tokens.

### `nuxt` argument

This argument is provided for your convenience and to allow using Nuxt context for invoking other composables. See the [Nuxt documentation](https://nuxt.com/docs/4.x/api/composables/use-nuxt-app) for more information.

## `onResponse(response, authState, nuxt)`

Handle the response and optionally instruct the module how to update state.

May return:
* `false` β€” stop further processing (module will not update auth state).
* `undefined` β€” proceed with default behaviour (e.g., the `signIn` flow will call `getSession` unless `signIn()` options say otherwise).
* `ResponseAccept` object β€” instruct the module what to set in `authState` (see below).
* Throw an `Error` to propagate a failure.

The `response` argument is the [`ofetch` raw response](https://github.com/unjs/ofetch?tab=readme-ov-file#-access-to-raw-response) that the module uses as well. `response._data` usually contains parsed body.

### `ResponseAccept` shape (what `onResponse` can return)

When `onResponse` returns an object (the `ResponseAccept`), it should conform to:

```ts
interface ResponseAccept<SessionDataType> {
token?: string | null // set or clear the access token in authState
refreshToken?: string | null // set or clear the refresh token in authState (if refresh is enabled)
session?: SessionDataType // set or clear the session object (when provided, `getSession` will NOT be called)
}
```

NuxtAuth will update `authState` accordingly, so you will be able to use the tokens in the later calls.
The tokens you return will be internally stored inside cookies and you can configure their Max-Age via module configuration.

When `token` is provided (not omitted and not `undefined`) the module will set `authState.token` (or clear it when `null`).
Same applies for `refreshToken` when refresh was enabled.

When `session` is provided the module will use that session directly and will **not** call `getSession`.

When the `onResponse` hook returns `undefined`, the module may call `getSession` (depending on the flow) to obtain the session.

### How different hooks handle return of `onResponse`

* **All hooks**
* `false` - stops the function execution, does not update anything or trigger any other logic.
* `throw Error` - executes `onError` hook if it was defined and then does function-specific logic (normally stops execution). Note that `onError` hook itself may throw an error if you want to propagate it to the calling place.
* `ResponseAccept<SessionDataType>` - see block above.

* **signIn**
* `throw Error` - stops the execution after calling `onError` hook if it was defined. We recommend you not throwing from `onError` hook of `signIn` as this function is also used inside middleware.

* **getSession**
* `throw Error` - does not stop the execution after calling `onError` hook if it was defined.
* We recommend you not throwing from `onError` hook of `getSession` as this function is also used inside middleware.
* When no `onError` hook was defined, the authentication state will be cleared (`data`, `rawToken`, `rawRefreshToken` set to `null`).
* The function will then continue its normal execution, potentially navigating the user away when `required` option was used during `getSession` function call.

* **signOut**
* `throw Error` - stops the execution after calling `onError` hook if it was defined.
* `undefined` - the authentication state will be cleared (`data`, `rawToken`, `rawRefreshToken` set to `null`).

* **signUp**
* `throw Error` - stops the execution after calling `onError` hook if it was defined. When no `onError` was defined, the error will be propagated to the caller.
* `undefined` - this will trigger `signIn` flow unless `preventLoginFlow` was given.

* **refresh**
* `throw Error` - stops the execution after calling `onError` hook if it was defined. When no `onError` was defined, the error will be propagated to the caller.
* `undefined` - this will trigger `getSession` call.

## `onError(errorCtx, authState, nuxt)`

### `errorCtx` argument

This is an `ErrorContext` object with:
* `error: Error` β€” the error which was thrown during request execution. The module guarantees the type and will return `new Error('Unknown error')` when the thrown value was not an instance of `Error`.
* `requestData: CreateRequestResult` β€” this is the exact object which was provided by the `createRequest` hook.
161 changes: 161 additions & 0 deletions docs/guide/hooks/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Hooks Provider examples

Note that examples here are intentionally simple to demonstrate the basics of how hooks work. For a complete example using all possible hooks and [Zod](https://zod.dev/) for validating the backend responses, refer to [playground-hooks demo](https://github.com/sidebase/nuxt-auth/blob/e2bda5784ddd325644fb8d73d0063b3cdf4b92b1/playground-hooks/config/hooks.ts).

## Basic `signIn` hook (body-based tokens)

This as an example for when your authentication backend uses POST Body to receive the credentials and tokens and to send session.

```ts
import { defineHooks } from '#imports'

export default defineHooks({
signIn: {
createRequest({ credentials }) {
return {
path: '/auth/login',
request: {
method: 'post',
body: credentials,
},
}
},

onResponse(response) {
// Backend returns { access: 'xxx', refresh: 'yyy', user: {...} }
const body = response._data
// Default to `undefined` to not reset the tokens and session (but you may want to reset it)
return {
token: body?.access ?? undefined,
refreshToken: body?.refresh ?? undefined,
session: body?.user ?? undefined,
}
},
},

getSession: {
createRequest(_getSessionOptions, authState) {
// Avoid calling `getSession` if no access token is present
if (authState.token.value === null) {
return false
}
// Call `/auth/profile` with the method of POST
// and access token sent via Body as { token }
return {
path: '/auth/profile',
request: {
method: 'post',
body: { token: authState.token.value },
},
}
},

onResponse(response) {
return {
session: response._data ?? null,
}
},
},
})
```

## Tokens returned in headers

This example demonstrates how to communicate with your authentication backend using headers.

```ts
export default defineHooks({
signIn: {
createRequest: ({ credentials }) => ({
path: '/auth/login',
request: { method: 'post', body: credentials },
}),

onResponse: (response) => {
const access = response.headers.get('x-access-token')
const refresh = response.headers.get('x-refresh-token')
// Don't return session β€” trigger a getSession call.
// Default to `undefined` to not reset the tokens.
return { token: access ?? undefined, refreshToken: refresh ?? undefined }
},
},

getSession: {
createRequest(_getSessionOptions, authState) {
// Avoid calling `getSession` if no access token is present
if (authState.token.value === null) {
return false
}
// Call `/auth/profile` with the method of GET
// and access token added to `Authorization` header
return {
path: '/auth/profile',
request: {
method: 'get',
headers: {
Authorization: `Bearer ${authState.token.value}`,
},
},
}
},
onResponse: response => ({ session: response._data ?? null }),
},
})
```

## Fully hijacking the flow

If your hook performs a redirect itself or sets cookies, you can stop the default flow by returning `false`:

```ts
defineHooksAdapter<Session>({
signIn: {
createRequest: data => ({ path: '/auth/login', request: { method: 'post', body: data.credentials } }),
async onResponse(response, authState, nuxt) {
// Handle everything yourself
authState.data.value = {}
authState.token.value = ''
// ...

return false
}
},
// ...
})
```

## My server returns HTTP-Only cookies

You are already almost set in this case - your browser will automatically send cookies with each request,
as soon as the cookies were configured with the correct domain and path on your server (as well as CORS).
NuxtAuth will use `getSession` to query your server - this is how your application will know the authentication status.

Please also note that `authState` will not have the tokens available in this case.

The correct way forward for you looks like this (simplified):

```ts
export default defineHooks({
// signIn: ...

getSession: {
createRequest() {
// Always call `getSession` as the module cannot see
// the tokens stored inside HTTP-Only cookies

// Call `/auth/profile` with the method of GET
// and no tokens provided - rely on browser including them
return {
path: '/auth/profile',
request: {
method: 'get',
// Explicitly include credentials to force browser to send cookies
credentials: 'include',
},
}
},
onResponse: response => ({ session: response._data ?? null }),
},
// ...
})
```
Loading
Loading