Skip to content

Commit

Permalink
fix(#990): revamp callbackUrl computation
Browse files Browse the repository at this point in the history
  • Loading branch information
phoenix-ru committed Feb 6, 2025
1 parent 172dc93 commit fff679c
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 66 deletions.
2 changes: 1 addition & 1 deletion playground-authjs/pages/protected/locally.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
})
</script>

Expand Down
23 changes: 13 additions & 10 deletions src/runtime/composables/authjs/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -81,11 +82,7 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = 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)

Expand Down Expand Up @@ -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: {
Expand All @@ -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'
})
}
Expand Down
21 changes: 11 additions & 10 deletions src/runtime/composables/local/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -63,15 +63,10 @@ const signIn: SignInFunc<Credentials, any> = 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 })
Expand Down Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions src/runtime/middleware/sidebase-auth.ts
Original file line number Diff line number Diff line change
@@ -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 | {
Expand Down Expand Up @@ -88,9 +89,14 @@ export default defineNuxtRouteMiddleware((to) => {
}

if (authConfig.provider.type === 'authjs') {
const signInOptions: Parameters<typeof signIn>[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<typeof signIn>[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<void>
}

Expand Down
116 changes: 116 additions & 0 deletions src/runtime/utils/callbackUrl.ts
Original file line number Diff line number Diff line change
@@ -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<string>
export async function determineCallbackUrl(
authConfig: AuthRuntimeConfigForCallbackUrl,
userCallbackUrl: string | undefined,
inferFromRequest?: false | undefined
): Promise<string | undefined>

/**
* 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<string | undefined> {
// 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<RouteMiddleware>[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
}
41 changes: 0 additions & 41 deletions src/runtime/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string | Promise<string>>(
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.
*
Expand Down

0 comments on commit fff679c

Please sign in to comment.