diff --git a/playground-authjs/pages/protected/locally.vue b/playground-authjs/pages/protected/locally.vue index c70d0356..dd3dbacf 100644 --- a/playground-authjs/pages/protected/locally.vue +++ b/playground-authjs/pages/protected/locally.vue @@ -3,7 +3,7 @@ import { definePageMeta } from '#imports' // Note: This is only for testing, it does not make sense to do this with `globalAppMiddleware` turned on definePageMeta({ - middleware: 'auth' + middleware: 'sidebase-auth' }) diff --git a/src/runtime/composables/authjs/useAuth.ts b/src/runtime/composables/authjs/useAuth.ts index 8110ae44..8688e466 100644 --- a/src/runtime/composables/authjs/useAuth.ts +++ b/src/runtime/composables/authjs/useAuth.ts @@ -2,12 +2,13 @@ import type { AppProvider, BuiltInProviderType } from 'next-auth/providers/index import { defu } from 'defu' import { type Ref, readonly } from 'vue' import { appendHeader } from 'h3' -import { determineCallbackUrl, resolveApiUrlPath } from '../../utils/url' +import { resolveApiUrlPath } from '../../utils/url' import { _fetch } from '../../utils/fetch' import { isNonEmptyObject } from '../../utils/checkSessionResult' import type { CommonUseAuthReturn, GetSessionOptions, SignInFunc, SignOutFunc } from '../../types' import { useTypedBackendConfig } from '../../helpers' import { getRequestURLWN } from '../common/getRequestURL' +import { determineCallbackUrl } from '../../utils/callbackUrl' import type { SessionData } from './useAuthState' import { navigateToAuthPageWN } from './utils/navigateToAuthPage' import type { NuxtApp } from '#app/nuxt' @@ -81,11 +82,7 @@ const signIn: SignInFunc = async (provider, op // 3. Redirect to the general sign-in page with all providers in case either no provider or no valid provider was selected const { redirect = true } = options ?? {} - let { callbackUrl } = options ?? {} - - if (typeof callbackUrl === 'undefined' && backendConfig.addDefaultCallbackUrl) { - callbackUrl = await determineCallbackUrl(runtimeConfig.public.auth, () => getRequestURLWN(nuxt)) - } + const callbackUrl = await determineCallbackUrl(runtimeConfig.public.auth, options?.callbackUrl) const signinUrl = resolveApiUrlPath('signin', runtimeConfig) @@ -240,16 +237,22 @@ function getSessionWithNuxt(nuxt: NuxtApp) { */ const signOut: SignOutFunc = async (options) => { const nuxt = useNuxtApp() + const runtimeConfig = useRuntimeConfig() - const requestURL = await getRequestURLWN(nuxt) - const { callbackUrl = requestURL, redirect = true } = options ?? {} + const { callbackUrl: userCallbackUrl, redirect = true } = options ?? {} const csrfToken = await getCsrfTokenWithNuxt(nuxt) + // Determine the correct callback URL + const callbackUrl = await determineCallbackUrl( + runtimeConfig.public.auth, + userCallbackUrl, + true + ) + if (!csrfToken) { throw createError({ statusCode: 400, statusMessage: 'Could not fetch CSRF Token for signing out' }) } - const callbackUrlFallback = requestURL const signoutData = await _fetch<{ url: string }>(nuxt, '/signout', { method: 'POST', headers: { @@ -259,7 +262,7 @@ const signOut: SignOutFunc = async (options) => { onRequest: ({ options }) => { options.body = new URLSearchParams({ csrfToken: csrfToken as string, - callbackUrl: callbackUrl || callbackUrlFallback, + callbackUrl, json: 'true' }) } diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index 9419758e..2c2dfa1e 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -3,9 +3,9 @@ import { type Ref, readonly } from 'vue' import type { CommonUseAuthReturn, GetSessionOptions, SecondarySignInOptions, SignInFunc, SignOutFunc, SignUpOptions } from '../../types' import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers' import { _fetch } from '../../utils/fetch' -import { determineCallbackUrl } from '../../utils/url' import { getRequestURLWN } from '../common/getRequestURL' import { ERROR_PREFIX } from '../../utils/logger' +import { determineCallbackUrl } from '../../utils/callbackUrl' import { formatToken } from './utils/token' import { type UseAuthStateReturn, useAuthState } from './useAuthState' import { callWithNuxt } from '#app/nuxt' @@ -63,15 +63,10 @@ const signIn: SignInFunc = async (credentials, signInOptions, } if (redirect) { - let { callbackUrl } = signInOptions ?? {} + let callbackUrl = signInOptions?.callbackUrl if (typeof callbackUrl === 'undefined') { const redirectQueryParam = useRoute()?.query?.redirect - if (redirectQueryParam) { - callbackUrl = redirectQueryParam.toString() - } - else { - callbackUrl = await determineCallbackUrl(runtimeConfig.public.auth, () => getRequestURLWN(nuxt)) - } + callbackUrl = await determineCallbackUrl(runtimeConfig.public.auth, redirectQueryParam?.toString()) } return navigateTo(callbackUrl, { external }) @@ -108,9 +103,15 @@ const signOut: SignOutFunc = async (signOutOptions) => { res = await _fetch(nuxt, path, { method, headers, body }) } - const { callbackUrl, redirect = true, external } = signOutOptions ?? {} + const { redirect = true, external } = signOutOptions ?? {} + if (redirect) { - await navigateTo(callbackUrl ?? await getRequestURLWN(nuxt), { external }) + let callbackUrl = signOutOptions?.callbackUrl + if (typeof callbackUrl === 'undefined') { + const redirectQueryParam = useRoute()?.query?.redirect + callbackUrl = await determineCallbackUrl(runtimeConfig.public.auth, redirectQueryParam?.toString(), true) + } + await navigateTo(callbackUrl, { external }) } return res diff --git a/src/runtime/middleware/sidebase-auth.ts b/src/runtime/middleware/sidebase-auth.ts index 76409d69..f5dd7d6b 100644 --- a/src/runtime/middleware/sidebase-auth.ts +++ b/src/runtime/middleware/sidebase-auth.ts @@ -1,6 +1,7 @@ -import { determineCallbackUrl, isExternalUrl } from '../utils/url' +import { isExternalUrl } from '../utils/url' import { isProduction } from '../helpers' import { ERROR_PREFIX } from '../utils/logger' +import { determineCallbackUrlForRouteMiddleware } from '../utils/callbackUrl' import { defineNuxtRouteMiddleware, navigateTo, useAuth, useRuntimeConfig } from '#imports' type MiddlewareMeta = boolean | { @@ -88,9 +89,14 @@ export default defineNuxtRouteMiddleware((to) => { } if (authConfig.provider.type === 'authjs') { - const signInOptions: Parameters[1] = { error: 'SessionRequired', callbackUrl: determineCallbackUrl(authConfig, () => to.fullPath) } - // eslint-disable-next-line ts/ban-ts-comment - // @ts-ignore This is valid for a backend-type of `authjs`, where sign-in accepts a provider as a first argument + const callbackUrl = determineCallbackUrlForRouteMiddleware(authConfig, to) + + const signInOptions: Parameters[1] = { + error: 'SessionRequired', + 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 } diff --git a/src/runtime/utils/callbackUrl.ts b/src/runtime/utils/callbackUrl.ts new file mode 100644 index 00000000..0acdad41 --- /dev/null +++ b/src/runtime/utils/callbackUrl.ts @@ -0,0 +1,116 @@ +import { getRequestURLWN } from '../composables/common/getRequestURL' +import type { RouteMiddleware } from '#app' +import { callWithNuxt, useNuxtApp, useRouter } from '#app' + +/** Slimmed down auth runtime config for `determineCallbackUrl` */ +interface AuthRuntimeConfigForCallbackUrl { + globalAppMiddleware: { + addDefaultCallbackUrl?: string | boolean + } | boolean +} + +// Overloads for better typing +export async function determineCallbackUrl( + authConfig: AuthRuntimeConfigForCallbackUrl, + userCallbackUrl: string | undefined, + inferFromRequest: true +): Promise +export async function determineCallbackUrl( + authConfig: AuthRuntimeConfigForCallbackUrl, + userCallbackUrl: string | undefined, + inferFromRequest?: false | undefined +): Promise + +/** + * Determines the desired callback url based on the users desires. Either: + * - uses a hardcoded path the user provided, + * - determines the callback based on the target the user wanted to reach + * + * @param authConfig Authentication runtime module config + * @param userCallbackUrl Callback URL provided by a user, e.g. as options to `signIn` + * @param inferFromRequest When `true`, will always do inference. + * When `false`, will never infer. + * When `undefined`, inference depends on `addDefaultCallbackUrl` + */ +export async function determineCallbackUrl( + authConfig: AuthRuntimeConfigForCallbackUrl, + userCallbackUrl: string | undefined, + inferFromRequest?: boolean | undefined +): Promise { + // Priority 1: User setting + if (userCallbackUrl) { + return await normalizeCallbackUrl(userCallbackUrl) + } + + // Priority 2: `addDefaultCallbackUrl` + const authConfigCallbackUrl = typeof authConfig.globalAppMiddleware === 'object' + ? authConfig.globalAppMiddleware.addDefaultCallbackUrl + : undefined + + // If a string value was set, always callback to it + if (typeof authConfigCallbackUrl === 'string') { + return await normalizeCallbackUrl(authConfigCallbackUrl) + } + + // Priority 3: Infer callback URL from the request + const shouldInferFromRequest = inferFromRequest !== false + && ( + inferFromRequest === true + || authConfigCallbackUrl === true + || (authConfigCallbackUrl === undefined && authConfig.globalAppMiddleware === true) + ) + + if (shouldInferFromRequest) { + const nuxt = useNuxtApp() + return getRequestURLWN(nuxt) + } +} + +// Avoid importing from `vue-router` directly +type RouteLocationNormalized = Parameters[0] + +/** + * Determines the correct callback URL for usage with Nuxt Route Middleware. + * The difference with a plain `determineCallbackUrl` is that this function produces + * non-normalized URLs. It is done because the result is being passed to `signIn` which does normalization. + * + * @param authConfig NuxtAuth module config (`runtimeConfig.public.auth`) + * @param middlewareTo The `to` parameter of NuxtRouteMiddleware + */ +export function determineCallbackUrlForRouteMiddleware( + authConfig: AuthRuntimeConfigForCallbackUrl, + middlewareTo: RouteLocationNormalized +): string | undefined { + const authConfigCallbackUrl = typeof authConfig.globalAppMiddleware === 'object' + ? authConfig.globalAppMiddleware.addDefaultCallbackUrl + : undefined + + // Priority 1: If a string value `addDefaultCallbackUrl` was set, always callback to it + if (typeof authConfigCallbackUrl === 'string') { + return authConfigCallbackUrl + } + + // Priority 2: `addDefaultCallbackUrl: true` or `globalAppMiddleware: true` + if ( + authConfigCallbackUrl === true + || (authConfigCallbackUrl === undefined && authConfig.globalAppMiddleware === true) + ) { + return middlewareTo.fullPath + } +} + +/** + * Normalizes the path by taking `app.baseURL` into account + * + * @see https://github.com/sidebase/nuxt-auth/issues/990#issuecomment-2630143443 + */ +async function normalizeCallbackUrl(rawCallbackUrl: string) { + const nuxt = useNuxtApp() + const router = await callWithNuxt(nuxt, useRouter) + + const resolvedUserRoute = router.resolve(rawCallbackUrl) + // no check for `resolvedUserRoute.matched` - prefer to show default 404 instead + + // Use `href` to include any possible `app.baseURL` + return resolvedUserRoute.href +} diff --git a/src/runtime/utils/url.ts b/src/runtime/utils/url.ts index b92b1f9f..4476d0d8 100644 --- a/src/runtime/utils/url.ts +++ b/src/runtime/utils/url.ts @@ -55,47 +55,6 @@ export function resolveApiBaseURL(runtimeConfig: RuntimeConfig, returnOnlyPathna return baseURL } -/** Slimmed down auth runtime config for `determineCallbackUrl` */ -interface AuthRuntimeConfigForCallbackUrl { - globalAppMiddleware: { - addDefaultCallbackUrl?: string | boolean - } | boolean -} - -/** - * Determines the desired callback url based on the users desires. Either: - * - uses a hardcoded path the user provided, - * - determines the callback based on the target the user wanted to reach - * - * @param authConfig Authentication runtime module config - * @param getOriginalTargetPath Function that returns the original location the user wanted to reach - */ -export function determineCallbackUrl>( - authConfig: AuthRuntimeConfigForCallbackUrl, - getOriginalTargetPath: () => T -): T | string | undefined { - const authConfigCallbackUrl = typeof authConfig.globalAppMiddleware === 'object' - ? authConfig.globalAppMiddleware.addDefaultCallbackUrl - : undefined - - if (typeof authConfigCallbackUrl !== 'undefined') { - // If string was set, always callback to that string - if (typeof authConfigCallbackUrl === 'string') { - return authConfigCallbackUrl - } - - // If boolean was set, set to current path if set to true - if (typeof authConfigCallbackUrl === 'boolean') { - if (authConfigCallbackUrl) { - return getOriginalTargetPath() - } - } - } - else if (authConfig.globalAppMiddleware === true) { - return getOriginalTargetPath() - } -} - /** * Naively checks if a URL is external or not by comparing against its protocol. *