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
3 changes: 2 additions & 1 deletion src/runtime/composables/authjs/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AppProvider, BuiltInProviderType } from 'next-auth/providers/index
import { defu } from 'defu'
import { readonly } from 'vue'
import type { Ref } from 'vue'
import type { NavigationFailure } from 'vue-router'
import { appendHeader } from 'h3'
import { resolveApiUrlPath } from '../../utils/url'
import { _fetch } from '../../utils/fetch'
Expand Down Expand Up @@ -35,7 +36,7 @@ interface SignInResult {
* 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
navigationResult: boolean | string | void | undefined | NavigationFailure
}

export interface SignInFunc {
Expand Down
80 changes: 57 additions & 23 deletions src/runtime/composables/authjs/utils/navigateToAuthPage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { NavigationFailure } from 'vue-router'
import { hasProtocol, isScriptProtocol } from 'ufo'
import { callWithNuxt, useRouter } from '#app'
import type { NuxtApp } from '#app'
Expand All @@ -6,7 +7,18 @@ export function navigateToAuthPageWN(nuxt: NuxtApp, href: string, isInternalRout
return callWithNuxt(nuxt, navigateToAuthPage, [nuxt, href, isInternalRouting])
}

const URL_QUOTE_RE = /"/g
// Adapted from https://github.com/nuxt/nuxt/blob/df18c4a8f1fa9b8577d3cc29a8965f6449adf698/packages/nuxt/src/app/composables/router.ts#L150-L160
const HTML_ATTR_UNSAFE_RE = /[&"'<>]/g
const HTML_ATTR_ENCODE_MAP: Record<string, string> = {
'&': '&amp;',
'"': '&quot;',
'\'': '&#x27;',
'<': '&lt;',
'>': '&gt;',
}
function encodeForHtmlAttr(value: string): string {
return value.replace(HTML_ATTR_UNSAFE_RE, c => HTML_ATTR_ENCODE_MAP[c]!)
}

/**
* Function to correctly navigate to auth-routes, necessary as the auth-routes are not part of the nuxt-app itself, so unknown to nuxt / vue-router.
Expand All @@ -16,36 +28,44 @@ 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/dc69e26c5b9adebab3bf4e39417288718b8ddf07/packages/nuxt/src/app/composables/router.ts#L130-L247
* Adapted from https://github.com/nuxt/nuxt/blob/df18c4a8f1fa9b8577d3cc29a8965f6449adf698/packages/nuxt/src/app/composables/router.ts#L162-L289
*
* @param nuxtApp Nuxt app context
* @param href HREF / URL to navigate to
*/
function navigateToAuthPage(nuxtApp: NuxtApp, href: string, isInternalRouting = false) {
const router = useRouter()
function navigateToAuthPage(nuxtApp: NuxtApp, href: string, isInternalRouting = false): string | boolean | Promise<string | boolean | undefined | void | NavigationFailure> {
// This is a slight difference with `nuxt/nuxt` - we treat `isInternalRouting` as `options.external`
// due to the routes being server-only, i.e. not resolvable app-side
const isExternalHost = hasProtocol(href, { acceptRelative: true })
const isExternal = isExternalHost || isInternalRouting
if (isExternal) {
const { protocol } = new URL(href, 'http://localhost')
if (protocol && isScriptProtocol(protocol)) {
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`)
}
}

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

// Early redirect on client-side
if (import.meta.client && !isExternal && inMiddleware) {
return href || '/'
}

const router = useRouter()

if (import.meta.server) {
if (nuxtApp.ssrContext) {
const isExternalHost = hasProtocol(href, { acceptRelative: true })
if (isExternalHost) {
const { protocol } = new URL(href, 'http://localhost')
if (protocol && isScriptProtocol(protocol)) {
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`)
}
}

// This is a difference with `nuxt/nuxt` - we do not add `app.baseURL` here because all consumers are responsible for it
// 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 || '/'
const location = isExternal ? href : router.resolve(href).fullPath || '/'

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)
const encodedLoc = encodeForHtmlAttr(encodedHeader)

nuxtApp.ssrContext!._renderResponse = {
statusCode: 302,
Expand All @@ -57,7 +77,7 @@ function navigateToAuthPage(nuxtApp: NuxtApp, href: string, isInternalRouting =

// We wait to perform the redirect last in case any other middleware will intercept the redirect
// and redirect somewhere else instead.
if (!isExternalHost && inMiddleware) {
if (!isExternal && 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)
Expand All @@ -69,17 +89,31 @@ function navigateToAuthPage(nuxtApp: NuxtApp, href: string, isInternalRouting =
}
}

window.location.href = href
// If href contains a hash, the browser does not reload the page. We reload manually.
// Client-side redirection using vue-router.
// The internal routes like `/api/auth/signin` are server-only so trying to `router.resolve` or `router.push` would 404
// Run any cleanup steps for the current scope, like ending BroadcastChannel
nuxtApp._scope.stop()

location.href = href
// If href contains a hash, the browser may not reload the page. We force reload manually.
if (href.includes('#')) {
window.location.reload()
location.reload()
}

// Wait for the `window.location.href` navigation from above to complete to avoid showing content. If that doesn't work fast enough, delegate navigation back to the `vue-router` (risking a vue-router 404 warning in the console, but still avoiding content-flashes of the protected target page)
const waitForNavigationWithFallbackToRouter = new Promise(resolve => setTimeout(resolve, 60 * 1000))
.then(() => router.push(href))

return waitForNavigationWithFallbackToRouter as Promise<void | undefined>
// Within a Nuxt route middleware handler
if (inMiddleware) {
// Abort navigation when app is hydrated
if (!nuxtApp.isHydrating) {
return false
}
// When app is hydrating (i.e. on page load), we don't want to abort navigation as
// it would lead to a 404 error / page that's blinking before location changes.
return new Promise(() => {})
}
// Note: We return a non-resolved promise here in contrast to Nuxt's `navigateTo` to keep the same behaviour
// of the middleware as was before with 60s timeout, see
// https://github.com/sidebase/nuxt-auth/blob/6daf2ad0290d338f152d192a1398923f61c3afdf/src/runtime/composables/authjs/utils/navigateToAuthPage.ts#L78-L82
return new Promise(() => {})
}

/**
Expand Down
Loading