Skip to content
Merged
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
66 changes: 38 additions & 28 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 All @@ -28,26 +29,24 @@ export type SupportedProviders = LiteralUnion<BuiltInProviderType> | undefined
* Utilities to make nested async composable calls play nicely with nuxt.
*
* Calling nested async composable can lead to "nuxt instance unavailable" errors. See more details here: https://github.com/nuxt/framework/issues/5740#issuecomment-1229197529. To resolve this we can manually ensure that the nuxt-context is set. This module contains `callWithNuxt` helpers for some of the methods that are frequently called in nested `useAuth` composable calls.
*
*/

// eslint-disable-next-line ts/no-empty-object-type
async function getRequestCookies(nuxt: NuxtApp): Promise<{ cookie: string } | {}> {
async function getRequestHeaders(nuxt: NuxtApp, includeCookie = true): Promise<{ cookie?: string, host?: string }> {
// `useRequestHeaders` is sync, so we narrow it to the awaited return type here
const { cookie } = await callWithNuxt(nuxt, () => useRequestHeaders(['cookie']))
if (cookie) {
return { cookie }
const headers = await callWithNuxt(nuxt, () => useRequestHeaders(['cookie', 'host']))
if (includeCookie && headers.cookie) {
return headers
}
return {}
return { host: headers.host }
}

/**
* Get the current Cross-Site Request Forgery token.
*
* You can use this to pass along for certain requests, most of the time you will not need it.
*/
async function getCsrfToken() {
const nuxt = useNuxtApp()
const headers = await getRequestCookies(nuxt)
const headers = await getRequestHeaders(nuxt)
return _fetch<{ csrfToken: string }>(nuxt, '/csrf', { headers }).then(response => response.csrfToken)
}
function getCsrfTokenWithNuxt(nuxt: NuxtApp) {
Expand All @@ -70,7 +69,7 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, op
const configuredProviders = await getProviders()
if (!configuredProviders) {
const errorUrl = resolveApiUrlPath('error', runtimeConfig)
return navigateToAuthPageWN(nuxt, errorUrl)
return navigateToAuthPageWN(nuxt, errorUrl, true)
}

// 2. If no `provider` was given, either use the configured `defaultProvider` or `undefined` (leading to a forward to the `/login` page with all providers)
Expand All @@ -83,23 +82,19 @@ 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 callWithNuxt(nuxt, () => determineCallbackUrl(runtimeConfig.public.auth, options?.callbackUrl))

const signinUrl = resolveApiUrlPath('signin', runtimeConfig)

const queryParams = callbackUrl ? `?${new URLSearchParams({ callbackUrl })}` : ''
const hrefSignInAllProviderPage = `${signinUrl}${queryParams}`
if (!provider) {
return navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage)
return navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage, true)
}

const selectedProvider = configuredProviders[provider]
if (!selectedProvider) {
return navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage)
return navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage, true)
}

// 4. Perform a sign-in straight away with the selected provider
Expand All @@ -114,9 +109,9 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, op

const csrfToken = await callWithNuxt(nuxt, getCsrfToken)

const headers: { 'Content-Type': string, 'cookie'?: string | undefined } = {
const headers: { 'Content-Type': string, 'cookie'?: string, 'host'?: string } = {
'Content-Type': 'application/x-www-form-urlencoded',
...(await getRequestCookies(nuxt))
...(await getRequestHeaders(nuxt))
}

// @ts-expect-error `options` is typed as any, but is a valid parameter for URLSearchParams
Expand Down Expand Up @@ -155,8 +150,16 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, op
/**
* Get all configured providers from the backend. You can use this method to build your own sign-in page.
*/
function getProviders() {
return _fetch<Record<Exclude<SupportedProviders, undefined>, Omit<AppProvider, 'options'> | undefined>>(useNuxtApp(), '/providers')
async function getProviders() {
const nuxt = useNuxtApp()
// Pass the `Host` header when making internal requests
const headers = await getRequestHeaders(nuxt, false)

return _fetch<Record<Exclude<SupportedProviders, undefined>, Omit<AppProvider, 'options'> | undefined>>(
nuxt,
'/providers',
{ headers }
)
}

/**
Expand All @@ -181,7 +184,7 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise<Sessio
loading.value = false
}

const headers = await getRequestCookies(nuxt)
const headers = await getRequestHeaders(nuxt)

return _fetch<SessionData>(nuxt, '/session', {
onResponse: ({ response }) => {
Expand Down Expand Up @@ -234,25 +237,32 @@ 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: {
'Content-Type': 'application/x-www-form-urlencoded'
'Content-Type': 'application/x-www-form-urlencoded',
...(await getRequestHeaders(nuxt))
},
onRequest: ({ options }) => {
options.body = new URLSearchParams({
csrfToken: csrfToken as string,
callbackUrl: callbackUrl || callbackUrlFallback,
callbackUrl,
json: 'true'
})
}
Expand Down
23 changes: 12 additions & 11 deletions src/runtime/composables/authjs/utils/navigateToAuthPage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { hasProtocol, isScriptProtocol, joinURL } from 'ufo'
import { type NuxtApp, abortNavigation, callWithNuxt, useNuxtApp, useRouter, useRuntimeConfig } from '#app'
import { hasProtocol, isScriptProtocol } from 'ufo'
import { type NuxtApp, abortNavigation, callWithNuxt, useRouter } from '#app'

export function navigateToAuthPageWN(nuxt: NuxtApp, href: string) {
return callWithNuxt(nuxt, navigateToAuthPage, [href])
export function navigateToAuthPageWN(nuxt: NuxtApp, href: string, isInternalRouting?: boolean) {
return callWithNuxt(nuxt, navigateToAuthPage, [nuxt, href, isInternalRouting])
}

const URL_QUOTE_RE = /"/g
Expand All @@ -17,14 +17,14 @@ const URL_QUOTE_RE = /"/g
*
* Adapted from https://github.com/nuxt/nuxt/blob/16d213bbdcc69c0cc72afb355755ff877654a374/packages/nuxt/src/app/composables/router.ts#L119-L217
*
* @param nuxt Nuxt app context
* @param href HREF / URL to navigate to
*/
export function navigateToAuthPage(href: string) {
function navigateToAuthPage(nuxt: NuxtApp, href: string, isInternalRouting = false) {
const router = useRouter()
const nuxtApp = useNuxtApp()

if (import.meta.server) {
if (nuxtApp.ssrContext) {
if (nuxt.ssrContext) {
const isExternalHost = hasProtocol(href, { acceptRelative: true })
if (isExternalHost) {
const { protocol } = new URL(href, 'http://localhost')
Expand All @@ -33,14 +33,15 @@ export function navigateToAuthPage(href: string) {
}
}

const fullPath = isExternalHost ? href : router.resolve(href).fullPath || '/'
const location = isExternalHost ? href : joinURL(useRuntimeConfig().app.baseURL, fullPath)
// 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 || '/'

// TODO: consider deprecating in favour of `app:rendered` and removing
return nuxtApp.callHook('app:redirected').then(() => {
return nuxt.callHook('app:redirected').then(() => {
const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost)
nuxtApp.ssrContext!._renderResponse = {
nuxt.ssrContext!._renderResponse = {
statusCode: 302,
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader },
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
9 changes: 6 additions & 3 deletions src/runtime/server/plugins/assertOrigin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
*/
import type { NitroApp } from 'nitropack/types'
import { ERROR_MESSAGES } from '../services/errors'
import { isProduction } from '../../helpers'
import { getServerOrigin } from '../services/utils'
import { isProduction, useTypedBackendConfig } from '../../helpers'
import { getServerBaseUrl } from '../services/authjs/utils'
import { useRuntimeConfig } from '#imports'

// type stub
type NitroAppPlugin = (nitro: NitroApp) => void
Expand All @@ -16,7 +17,9 @@ function defineNitroPlugin(def: NitroAppPlugin): NitroAppPlugin {
// Export runtime plugin
export default defineNitroPlugin(() => {
try {
getServerOrigin()
const runtimeConfig = useRuntimeConfig()
const trustHostUserPreference = useTypedBackendConfig(runtimeConfig, 'authjs').trustHost
getServerBaseUrl(runtimeConfig, false, trustHostUserPreference, isProduction)
}
catch (error) {
if (!isProduction) {
Expand Down
Loading