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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/guide/application-side/protecting-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,15 @@ export default defineNuxtRouteMiddleware((to) => {
* We cannot directly call and/or return `signIn` here as `signIn` uses async composables under the hood, leading to "nuxt instance undefined errors", see https://github.com/nuxt/framework/issues/5740#issuecomment-1229197529
*
* So to avoid calling it, we return it immediately.
*
* Important: you need to explicitly handle the value returned by the `signIn`,
* for example by changing it to `false` (to abort further navigation) or `undefined` (to process other middleware).
* See https://github.com/sidebase/nuxt-auth/issues/1042
*/
return signIn(undefined, { callbackUrl: to.path }) as ReturnType<typeof navigateTo>
return signIn(
undefined,
{ callbackUrl: to.path }
).then(() => /* abort further route navigation */ false)
})
```

Expand Down
23 changes: 16 additions & 7 deletions src/runtime/composables/authjs/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ interface SignInResult {
status: number
ok: boolean
url: any
/**
* Result returned by `navigateToAuthPage`, which needs to be passed back to vue-router by the middleware.
* @see https://github.com/sidebase/nuxt-auth/pull/1057
*/
navigationResult: boolean | string | void | undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a. Any chance we can make this type more speaking? eg making this a string-union with speaking members for different known results?
b. can/should we document this somewhere? either inside the docs or as a docstring in-code that explains the different variants and what this key means?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a. Not really, as this is a value returned by either navigateToAuthPageWN or vue-router. I would prefer to modify this value as little as possible, since we need to immediately pass it back to vue-router.

b. I can add a doctext explaining what this value is

}

export interface SignInFunc {
Expand Down Expand Up @@ -92,15 +97,16 @@ export function useAuth(): UseAuthReturn {
const configuredProviders = await getProviders()
if (!configuredProviders) {
const errorUrl = resolveApiUrlPath('error', runtimeConfig)
await navigateToAuthPageWN(nuxt, errorUrl, true)
const navigationResult = await navigateToAuthPageWN(nuxt, errorUrl, true)

return {
// Future AuthJS compat here and in other places
// https://authjs.dev/reference/core/errors#invalidprovider
error: 'InvalidProvider',
ok: false,
status: 500,
url: errorUrl
url: errorUrl,
navigationResult,
}
}

Expand All @@ -122,14 +128,15 @@ export function useAuth(): UseAuthReturn {

const selectedProvider = provider && configuredProviders[provider]
if (!selectedProvider) {
await navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage, true)
const navigationResult = await navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage, true)

return {
// https://authjs.dev/reference/core/errors#invalidprovider
error: 'InvalidProvider',
ok: false,
status: 400,
url: hrefSignInAllProviderPage
url: hrefSignInAllProviderPage,
navigationResult,
}
}

Expand Down Expand Up @@ -165,7 +172,7 @@ export function useAuth(): UseAuthReturn {

if (redirect || !isSupportingReturn) {
const href = data.url ?? callbackUrl
await navigateToAuthPageWN(nuxt, href)
const navigationResult = await navigateToAuthPageWN(nuxt, href)

// We use `http://_` as a base to allow relative URLs in `callbackUrl`. We only need the `error` query param
const error = new URL(href, 'http://_').searchParams.get('error')
Expand All @@ -174,7 +181,8 @@ export function useAuth(): UseAuthReturn {
error,
ok: true,
status: 302,
url: href
url: href,
navigationResult,
}
}

Expand All @@ -186,7 +194,8 @@ export function useAuth(): UseAuthReturn {
error,
status: 200,
ok: true,
url: error ? null : data.url
url: error ? null : data.url,
navigationResult: undefined,
}
}

Expand Down
37 changes: 27 additions & 10 deletions src/runtime/composables/authjs/utils/navigateToAuthPage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { hasProtocol, isScriptProtocol } from 'ufo'
import { abortNavigation, callWithNuxt, useRouter } from '#app'
import { callWithNuxt, useRouter } from '#app'
import type { NuxtApp } from '#app'

export function navigateToAuthPageWN(nuxt: NuxtApp, href: string, isInternalRouting?: boolean) {
Expand All @@ -16,16 +16,19 @@ const URL_QUOTE_RE = /"/g
* manually set `window.location.href` on the client **and then fake return a Promise that does not immediately resolve to block navigation (although it will not actually be fully awaited, but just be awaited long enough for the naviation to complete)**.
* 2. Additionally on the server-side, we cannot use `navigateTo(signInUrl)` as this uses `vue-router` internally which does not know the "external" sign-in page of next-auth and thus will log a warning which we want to avoid.
*
* Adapted from https://github.com/nuxt/nuxt/blob/16d213bbdcc69c0cc72afb355755ff877654a374/packages/nuxt/src/app/composables/router.ts#L119-L217
* Adapted from https://github.com/nuxt/nuxt/blob/dc69e26c5b9adebab3bf4e39417288718b8ddf07/packages/nuxt/src/app/composables/router.ts#L130-L247
*
* @param nuxt Nuxt app context
* @param nuxtApp Nuxt app context
* @param href HREF / URL to navigate to
*/
function navigateToAuthPage(nuxt: NuxtApp, href: string, isInternalRouting = false) {
function navigateToAuthPage(nuxtApp: NuxtApp, href: string, isInternalRouting = false) {
const router = useRouter()

// https://github.com/nuxt/nuxt/blob/dc69e26c5b9adebab3bf4e39417288718b8ddf07/packages/nuxt/src/app/composables/router.ts#L84-L93
const inMiddleware = Boolean(nuxtApp._processingMiddleware)

if (import.meta.server) {
if (nuxt.ssrContext) {
if (nuxtApp.ssrContext) {
const isExternalHost = hasProtocol(href, { acceptRelative: true })
if (isExternalHost) {
const { protocol } = new URL(href, 'http://localhost')
Expand All @@ -38,17 +41,31 @@ function navigateToAuthPage(nuxt: NuxtApp, href: string, isInternalRouting = fal
// We also skip resolution for internal routing to avoid triggering `No match found` warning from Vue Router
const location = isExternalHost || isInternalRouting ? href : router.resolve(href).fullPath || '/'

// TODO: consider deprecating in favour of `app:rendered` and removing
return nuxt.callHook('app:redirected').then(() => {
async function redirect(response: false | undefined) {
// TODO: consider deprecating in favour of `app:rendered` and removing
await nuxtApp.callHook('app:redirected')
const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost)
nuxt.ssrContext!._renderResponse = {

nuxtApp.ssrContext!._renderResponse = {
statusCode: 302,
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader },
}
abortNavigation()
})
return response
}

// We wait to perform the redirect last in case any other middleware will intercept the redirect
// and redirect somewhere else instead.
if (!isExternalHost && inMiddleware) {
// For an unknown reason, `final.fullPath` received here is not percent-encoded, leading to the check always failing.
// To preserve compatibility with NuxtAuth < 1.0, we simply return `undefined`.
// TODO: Find the reason or report the issue to Nuxt if `navigateTo` has the same problem (`router.resolve` handles the `%2F` in callback URL correctly)
// router.afterEach(final => final.fullPath === location ? redirect(false) : undefined)
// return href
Comment on lines +64 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this code leftover/dead? it doesnt seem to belong to the comment above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this code was commented exactly due to the explanation above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didnt get that, a colon would be helpful! But alas: definitely a nit, wont block the merge for this (:

Copy link
Collaborator Author

@phoenix-ru phoenix-ru Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: The problem is also present in Nuxt's navigateTo, I am publishing a reproduction

Their implementation gets the same problem:

Redirect was called { fullPath: '/redirect?callback=%2Fother', 'final.fullPath': '/redirect?callback=/other', 'final.fullPath === fullPath': false } 
Copy link
Collaborator Author

@phoenix-ru phoenix-ru Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduction repo: https://github.com/phoenix-ru/nuxt-navigate-to-fullpath-reproduction

Issue will be created shortly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please link it here as well!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue created: nuxt/nuxt#33273

return redirect(undefined)
}
return redirect(!inMiddleware ? undefined : /* abort further route navigation */ false)
}
}

Expand Down
19 changes: 17 additions & 2 deletions src/runtime/middleware/sidebase-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,23 @@ export default defineNuxtRouteMiddleware((to) => {
callbackUrl
}

// @ts-expect-error This is valid for a backend-type of `authjs`, where sign-in accepts a provider as a first argument
return signIn(undefined, signInOptions) as Promise<void>
return signIn(
// @ts-expect-error This is valid for a backend-type of `authjs`, where sign-in accepts a provider as a first argument
undefined,
signInOptions
).then((signInResult) => {
// `signIn` function automatically navigates to the correct page,
// we need to tell `vue-router` what page we navigated to by returning the value.
if (signInResult) {
return signInResult.navigationResult
}

// When no result was provided, allow other middleware to run by default.
// When `false` is used, other middleware will be skipped.
// See: https://router.vuejs.org/guide/advanced/navigation-guards.html#Global-Before-Guards
// See: https://github.com/nuxt/nuxt/blob/dc69e26c5b9adebab3bf4e39417288718b8ddf07/packages/nuxt/src/pages/runtime/plugins/router.ts#L241-L250
return true
})
}

const loginPage = authConfig.provider.pages.login
Expand Down
Loading