Skip to content

Commit

Permalink
fix(#990): use a dedicated host calculation for authjs
Browse files Browse the repository at this point in the history
  • Loading branch information
phoenix-ru committed Jan 24, 2025
1 parent c3e9d54 commit bc65dc1
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 89 deletions.
6 changes: 4 additions & 2 deletions src/runtime/server/plugins/assertOrigin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import type { NitroApp } from 'nitropack/types'
import { ERROR_MESSAGES } from '../services/errors'
import { isProduction } from '../../helpers'
import { getServerOrigin } from '../services/utils'
import { getServerOrigin } from '../services/authjs/utils'
import { useRuntimeConfig } from '#imports'

// type stub
type NitroAppPlugin = (nitro: NitroApp) => void
Expand All @@ -16,7 +17,8 @@ function defineNitroPlugin(def: NitroAppPlugin): NitroAppPlugin {
// Export runtime plugin
export default defineNitroPlugin(() => {
try {
getServerOrigin()
const runtimeConfig = useRuntimeConfig()
getServerOrigin(runtimeConfig)
}
catch (error) {
if (!isProduction) {
Expand Down
70 changes: 16 additions & 54 deletions src/runtime/server/services/authjs/nuxtAuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import { defu } from 'defu'
import { joinURL } from 'ufo'
import { ERROR_MESSAGES } from '../errors'
import { isNonEmptyObject } from '../../../utils/checkSessionResult'
import { getServerOrigin } from '../utils'
import { useTypedBackendConfig } from '../../../helpers'
import { resolveApiBaseURL } from '../../../utils/url'
import { getHostValueForAuthjs, getServerOrigin } from './utils'
import { useRuntimeConfig } from '#imports'

type RuntimeConfig = ReturnType<typeof useRuntimeConfig>

let preparedAuthjsHandler: ((req: RequestInternal) => Promise<ResponseInternal>) | undefined
let usedSecret: string | undefined

Expand Down Expand Up @@ -64,7 +66,7 @@ export function NuxtAuthHandler(nuxtAuthOptions?: AuthOptions) {
const { res } = event.node

// 1.1. Assemble and perform request to the NextAuth.js auth handler
const nextRequest = await createRequestForAuthjs(event, trustHostUserPreference)
const nextRequest = await createRequestForAuthjs(event, runtimeConfig, trustHostUserPreference)

// 1.2. Call Authjs
// Safety: `preparedAuthjsHandler` was assigned earlier and never re-assigned
Expand Down Expand Up @@ -122,10 +124,6 @@ export async function getServerSession(event: H3Event) {
}
}

// Build a correct endpoint
const sessionUrlBase = getRequestBaseFromH3Event(event, trustHostUserPreference)
const sessionUrl = new URL(sessionUrlPath, sessionUrlBase)

// Create a virtual Request to check the session
const authjsRequest: RequestInternal = {
action: 'session',
Expand All @@ -135,8 +133,8 @@ export async function getServerSession(event: H3Event) {
cookies: parseCookies(event),
providerId: undefined,
error: undefined,
host: sessionUrl.href,
query: Object.fromEntries(sessionUrl.searchParams)
host: getHostValueForAuthjs(event, runtimeConfig, trustHostUserPreference),
query: {}
}

// Invoke Auth.js
Expand All @@ -163,14 +161,16 @@ export async function getServerSession(event: H3Event) {
* @param eventAndOptions.secret A secret string used for encryption
*/
export function getToken<R extends boolean = false>({ event, secureCookie, secret, ...rest }: Omit<GetTokenParams<R>, 'req'> & { event: H3Event }) {
const runtimeConfig = useRuntimeConfig()

return authjsGetToken({
// @ts-expect-error As our request is not a real next-auth request, we pass down only what's required for the method, as per code from https://github.com/nextauthjs/next-auth/blob/8387c78e3fef13350d8a8c6102caeeb05c70a650/packages/next-auth/src/jwt/index.ts#L68
req: {
cookies: parseCookies(event),
headers: getHeaders(event) as IncomingHttpHeaders
},
// see https://github.com/nextauthjs/next-auth/blob/8387c78e3fef13350d8a8c6102caeeb05c70a650/packages/next-auth/src/jwt/index.ts#L73
secureCookie: secureCookie ?? getServerOrigin(event).startsWith('https://'),
secureCookie: secureCookie ?? getServerOrigin(runtimeConfig, event).startsWith('https://'),
secret: secret || usedSecret,
...rest
})
Expand All @@ -182,9 +182,14 @@ export function getToken<R extends boolean = false>({ event, secureCookie, secre
*
* @param event H3Event to transform into `RequestInternal`
*/
async function createRequestForAuthjs(event: H3Event, trustHostUserPreference: boolean): Promise<RequestInternal> {
async function createRequestForAuthjs(
event: H3Event,
runtimeConfig: RuntimeConfig,
trustHostUserPreference: boolean
): Promise<RequestInternal> {
const nextRequest: Omit<RequestInternal, 'action'> = {
host: getRequestURLFromH3Event(event, trustHostUserPreference).href,
// `authjs` expects the baseURL here despite the param name
host: getHostValueForAuthjs(event, runtimeConfig, trustHostUserPreference),
body: undefined,
cookies: parseCookies(event),
query: undefined,
Expand Down Expand Up @@ -217,49 +222,6 @@ async function createRequestForAuthjs(event: H3Event, trustHostUserPreference: b
}
}

/**
* Get the request url or construct it.
* Adapted from `h3` to also account for server origin.
*
* ## WARNING
* Please ensure that any URL produced by this function has a trusted host!
*
* @param event The H3 Event containing the request
* @param trustHost Whether the host can be trusted. If `true`, base will be inferred from the request, otherwise the configured origin will be used.
* @throws {Error} When server origin was incorrectly configured or when URL building failed
*/
function getRequestURLFromH3Event(event: H3Event, trustHost: boolean): URL {
const path = (event.node.req.originalUrl || event.path).replace(
/^[/\\]+/g,
'/'
)
const base = getRequestBaseFromH3Event(event, trustHost)
return new URL(path, base)
}

/**
* Gets the request base in the form of origin.
*
* ## WARNING
* Please ensure that any URL produced by this function has a trusted host!
*
* @param event The H3 Event containing the request
* @param trustHost Whether the host can be trusted. If `true`, base will be inferred from the request, otherwise the configured origin will be used.
* @throws {Error} When server origin was incorrectly configured
*/
function getRequestBaseFromH3Event(event: H3Event, trustHost: boolean): string {
if (trustHost) {
const host = getRequestHost(event, { xForwardedHost: trustHost })
const protocol = getRequestProtocol(event)

return `${protocol}://${host}`
}
// This may throw, we don't catch it
const origin = getServerOrigin(event)

return origin
}

/** Actions supported by auth handler */
const SUPPORTED_ACTIONS: AuthAction[] = ['providers', 'session', 'csrf', 'signin', 'signout', 'callback', 'verify-request', 'error', '_log']

Expand Down
78 changes: 78 additions & 0 deletions src/runtime/server/services/authjs/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { H3Event } from 'h3'
import getURL from 'requrl'
import { parseURL } from 'ufo'
import { isProduction } from '../../../helpers'
import { type RuntimeConfig, resolveApiBaseURL } from '../../../utils/url'
import { ERROR_MESSAGES } from '../errors'

/**
* Gets the correct value of `host` (later renamed to `origin`) configuration parameter for `authjs` InternalRequest.
* This is actually neither `Host` nor `Origin`, but a base URL (`authjs` naming is misleading) including path.
*
* When user specifies `trustHost`, we would use the `event` to compute the base URL by using full request URL minus the `/action` and `/provider` parts.
*
* ## WARNING
* Please ensure that any URL produced by this function has a trusted host!
*
* @example
* ```
* // Without `trustHost`
* // event path = https://example.com/auth/path/signin/github?callbackUrl=foo
* // configured baseURL = https://your.domain/api/auth
* getHostValueForAuthjs(event, runtimeConfig, false) === 'https://your.domain/api/auth'
*
* // With `trustHost`
* // event path = https://example.com/auth/path/signin/github?callbackUrl=foo
* getHostValueForAuthjs(event, runtimeConfig, true) === 'https://example.com/auth/path'
* ```
*
* @param event The H3 Event containing the request
* @param runtimeConfig Nuxt RuntimeConfig
* @param trustHost Whether the host can be trusted. If `true`, base will be inferred from the request, otherwise the configured origin will be used. * @returns {string} Value formatted for usage with Authjs
* @throws {Error} When server origin was incorrectly configured or when URL building failed
*/
export function getHostValueForAuthjs(
event: H3Event,
runtimeConfig: RuntimeConfig,
trustHost: boolean,
): string {
if (trustHost) {
return getServerBaseUrl(runtimeConfig, true, event)
}

return resolveApiBaseURL(runtimeConfig, false)
}

/**
* Get `origin` and fallback to `x-forwarded-host` or `host` headers if not in production.
*/
export function getServerOrigin(runtimeConfig: RuntimeConfig, event?: H3Event): string {
return getServerBaseUrl(runtimeConfig, false, event)
}

function getServerBaseUrl(
runtimeConfig: RuntimeConfig,
includePath: boolean,
event?: H3Event,
): string {
// Prio 1: Environment variable
// Prio 2: Static configuration

// Resolve the value from runtime config/env.
// If the returned value has protocol and host, it is considered valid.
const baseURL = resolveApiBaseURL(runtimeConfig, false)
const parsed = parseURL(baseURL)
if (parsed.protocol && parsed.host) {
const base = `${parsed.protocol}//${parsed.host}`
return includePath
? `${base}${parsed.pathname}${parsed.search || ''}${parsed.hash || ''}`
: base
}

// Prio 3: Try to infer the origin if we're not in production
if (event && !isProduction) {
return getURL(event.node.req, includePath)
}

throw new Error(ERROR_MESSAGES.NO_ORIGIN)
}
32 changes: 0 additions & 32 deletions src/runtime/server/services/utils.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/runtime/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { joinURL, parseURL, withLeadingSlash } from 'ufo'

// Slimmed down type to allow easy unit testing
interface RuntimeConfig {
export interface RuntimeConfig {
public: {
auth: {
baseURL: string
Expand Down

0 comments on commit bc65dc1

Please sign in to comment.