Skip to content
Draft
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
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
14 changes: 14 additions & 0 deletions docs/.vitepress/routes/sidebar/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ export const routes: DefaultTheme.SidebarItem[] = [
}
],
},
{
text: 'Hooks Provider',
base: '/guide/hooks',
items: [
{
text: 'Quick Start',
link: '/quick-start',
},
{
text: 'Examples',
link: '/examples',
}
],
},
{
text: 'Advanced',
base: '/guide/advanced',
Expand Down
89 changes: 89 additions & 0 deletions docs/guide/hooks/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Hooks Provider examples

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

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

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

async onResponse(response) {
// Backend returns { access: 'xxx', refresh: 'yyy', user: {...} }
const body = response._data
return {
token: body?.access ?? undefined,
refreshToken: body?.refresh ?? undefined,
session: body?.user ?? undefined,
}
},
},

getSession: {
async createRequest() {
return {
path: '/auth/profile',
request: {
method: 'get',
},
}
},

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

## Tokens returned in 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
return { token: access ?? undefined, refreshToken: refresh ?? undefined }
},
},

getSession: {
createRequest: () => ({ path: '/auth/profile', request: { method: 'get' } }),
onResponse: (response) => 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
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
}
}
```
166 changes: 166 additions & 0 deletions docs/guide/hooks/quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Hooks provider

The Hooks Provider is an advanced and highly flexible provider intended for use with external authentication backends.

Its main difference with Local Provider is that it does not ship any default implementation and instead relies on you providing an adapter for communicating with your backend. You get complete control over how requests are built and how responses are used.

## Configuration

In `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
auth: {
provider: {
type: 'hooks',
adapter: '~/app/nuxt-auth-adapter.ts',
},
},
})
````

The path should point to a file that exports an adapter implementing `Hooks`.

## Adapter

### Quick example

Here's a quick minimal example of an adapter. Only `signIn` and `getSession` endpoints are required:

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

onResponse: (response) => {
// Backend returns { access: 'xxx', refresh: 'yyy', user: {...} }
const body = response._data
return {
token: body?.access ?? undefined,
refreshToken: body?.refresh ?? undefined,
session: body?.user ?? undefined,
}
},
},

getSession: {
createRequest: () => ({
path: '/auth/profile',
request: { method: 'get' }
}),
onResponse: (response) => response._data ?? null,
},
})
```

### 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 two functions: `createRequest` and `onResponse`.

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

Prepare data for the fetch call.

Must return either an object:

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

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

#### `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 can contain:

```ts
{
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?: any | null // set or clear the session object (when provided, `getSession` will NOT be called)
}
```

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.

#### `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.

### In short

* `createRequest` builds and returns `{ path, request }`. The module will call `_fetchRaw(nuxt, path, request)`.

* `onResponse` determines what the module should do next:
* `false` β€” stop everything (useful when the hook itself handled redirects, cookies or state changes).
* `undefined` β€” default behaviour (module may call `getSession`).
* `{ token?, refreshToken?, session? }` β€” module will set provided tokens/session in `authState`.

## Pages

Configure the path of the login-page that the user should be redirected to, when they try to access a protected page without being logged in. This page will also not be blocked by the global middleware.

```ts
export default defineNuxtConfig({
// previous configuration
auth: {
provider: {
type: 'hooks',
pages: {
login: '/login'
}
}
}
})
```

## Some tips

* When your backend uses **HTTP-only cookies** for session management, prefer returning `undefined` from `onResponse` β€” browsers will automatically include cookies; the module will call `getSession` to obtain the user object when needed.
* If your backend is cross-origin, remember to configure CORS and allow credentials:

* `Access-Control-Allow-Credentials: true`
* `Access-Control-Allow-Origin: <your-front-end-origin>` (cannot be `*` when credentials are used)
* The default hooks shipped with the module try to extract tokens using the configured token pointers (`token.signInResponseTokenPointer`) and headers. Use hooks only when you need more customization.

57 changes: 57 additions & 0 deletions src/runtime/composables/hooks/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { HooksAdapter } from './types'

export function defineHooksAdapter<SessionDataType = unknown>(hooks: HooksAdapter<SessionDataType>): HooksAdapter<SessionDataType> {
return hooks
}

interface Session {
// Data of users returned by `getSession` endpoint
}

export default defineHooksAdapter<Session>({
signIn: {
createRequest(credentials, authState, nuxt) {
// todo

return {
path: '',
request: {
body: credentials,
}
}
},

onResponse(response, authState, nuxt) {
// Possible return values:
// - false - skip any further logic (useful when onResponse handles everything);
// - {} - skip assigning tokens and session, but still possibly call getSession and redirect
// - { token: string } - assign token and continue as normal;
// - { token: string, session: object } - assign token, skip calling getSession, but do possibly call redirect;

// todo
return {

}
},
},

getSession: {
createRequest(data, authState, nuxt) {
// todo

return {
path: '',
request: {}
}
},

onResponse(response, authState, nuxt) {
return response._data as Session
}
},

// signOut: {
//
// }
})

Loading
Loading