-
-
Notifications
You must be signed in to change notification settings - Fork 190
feat(#964): Add Hooks provider #1062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0c38105
a1e73ce
0b15c6f
37da522
591ecb0
860d592
89c9d51
ac6b958
454a698
7288895
4422a44
e2bda57
c416c1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
Comment on lines
+53
to
+55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a schema for this or this is e.g. based off the session object you can provide via the nuxt config? |
||
|
|
||
| ### `nuxt` argument | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we call this
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to add a guide on how you can use this to e.g. access |
||
|
|
||
| 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). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this |
||
| * `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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What else can |
||
|
|
||
| ### `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. | ||
| 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, | ||
| } | ||
| }, | ||
|
Comment on lines
+24
to
+33
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these hooks promises? E.g. if I wanted to do another DB or API call here, would the hooks provider support this? |
||
| }, | ||
|
|
||
| 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I return
|
||
| 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 }), | ||
| }, | ||
| // ... | ||
| }) | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you decide to abstract the returns of the hooks provider? While I think it is a good step forward compared to the current
local-provider I think it could still limit you if you e.g. want to switch fromofetchtoaxiosunder the hood.