From b645c75ade1008dbf3c9f5a38a46a16d4861fdd9 Mon Sep 17 00:00:00 2001 From: Rob Marscher Date: Thu, 4 Jan 2024 15:29:47 -0500 Subject: [PATCH] Detect if cross-site cookie config is needed Necessary if API_URL does not contain APP_URL. --- packages/api/src/auth/index.ts | 27 +++++++++++++++++++++------ packages/api/src/auth/oauth.ts | 7 +++---- packages/api/src/context.ts | 2 +- packages/api/src/routes/user.ts | 5 ++--- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts index 47cbed5b3..49201546d 100644 --- a/packages/api/src/auth/index.ts +++ b/packages/api/src/auth/index.ts @@ -2,6 +2,7 @@ import { Adapter, DatabaseSessionAttributes, DatabaseUserAttributes, Lucia, Time import { DrizzleSQLiteAdapter } from '@lucia-auth/adapter-drizzle' import { SessionTable, UserTable } from '../db/schema' import { DB } from '../db/client' +import type { ApiContextProps } from '../context' /** * Lucia's isValidRequestOrigin method will compare the @@ -18,17 +19,30 @@ export const getAllowedOriginHost = (app_url: string, request?: Request) => { return requestHost === appHost ? appHost : undefined } -export const createAuth = (db: DB, appUrl: string) => { +export const isCrossDomain = (appUrl?: string, apiUrl?: string) => { + if (!appUrl || !apiUrl) return true + const appHost = new URL(appUrl).host + const apiHost = new URL(apiUrl).host + return !apiHost.endsWith(appHost) +} + +export function getCookieOptions(ctx: ApiContextProps) { + return isCrossDomain(ctx.env.APP_URL, ctx.env.PUBLIC_API_URL) + ? 'HttpOnly; SameSite=None; Secure;' + : 'HttpOnly; SameSite=Lax; Secure;' +} + +export const createAuth = (db: DB, appUrl: string, apiUrl: string) => { // @ts-ignore Expect type errors because this is D1 and not SQLite... but it works const adapter = new DrizzleSQLiteAdapter(db, SessionTable, UserTable) // cast probably only needed until adapter-drizzle is updated + // @ts-ignore the "none" option for sameSite works... but https://github.com/lucia-auth/lucia/issues/1320 return new Lucia(adapter as Adapter, { - ...getAuthOptions(appUrl), + ...getAuthOptions(appUrl, apiUrl), }) } -export const getAuthOptions = (appUrl: string) => { - const env = !appUrl || appUrl.startsWith('http:') ? 'DEV' : 'PROD' +export const getAuthOptions = (appUrl: string, apiUrl: string) => { return { getUserAttributes: (data: DatabaseUserAttributes) => { return { @@ -45,8 +59,9 @@ export const getAuthOptions = (appUrl: string) => { name: 'auth_session', expires: false, attributes: { - secure: env === 'PROD', - sameSite: 'lax' as const, + secure: true, + // This might not work forever https://github.com/lucia-auth/lucia/issues/1320 + sameSite: isCrossDomain(appUrl, apiUrl) ? ('none' as const) : ('lax' as const), }, }, diff --git a/packages/api/src/auth/oauth.ts b/packages/api/src/auth/oauth.ts index 20063830f..64ab0fece 100644 --- a/packages/api/src/auth/oauth.ts +++ b/packages/api/src/auth/oauth.ts @@ -26,6 +26,7 @@ import { createAuthMethodId, createUser, getAuthMethod, getUserById } from './us import { P, match } from 'ts-pattern' import { getCookie } from 'hono/cookie' import { TRPCError } from '@trpc/server' +import { getCookieOptions, isCrossDomain } from '.' export interface AppleIdTokenClaims { iss: 'https://appleid.apple.com' @@ -101,15 +102,13 @@ export const getAuthorizationUrl = async (ctx: ApiContextProps, service: AuthPro const provider = getAuthProvider(ctx, service) const secure = ctx.req?.url.startsWith('https:') ? 'Secure; ' : '' const state = generateState() - ctx.setCookie( - `${service}_oauth_state=${state}; Path=/; ${secure}HttpOnly; SameSite=Lax; Max-Age=600` - ) + ctx.setCookie(`${service}_oauth_state=${state}; Path=/; ${getCookieOptions(ctx)} Max-Age=600`) return await match({ provider, service }) .with({ service: 'google', provider: P.instanceOf(Google) }, async ({ provider }) => { // Google requires PKCE const codeVerifier = generateCodeVerifier() ctx.setCookie( - `${service}_oauth_verifier=${codeVerifier}; Path=/; ${secure}HttpOnly; SameSite=Lax; Max-Age=600` + `${service}_oauth_verifier=${codeVerifier}; Path=/; ${getCookieOptions(ctx)} Max-Age=600` ) const url = await provider.createAuthorizationURL(state, codeVerifier, { scopes: ['https://www.googleapis.com/auth/userinfo.email'], diff --git a/packages/api/src/context.ts b/packages/api/src/context.ts index c4be329a7..d99970139 100644 --- a/packages/api/src/context.ts +++ b/packages/api/src/context.ts @@ -64,7 +64,7 @@ export const createContext = async ( // const user = await getUser() - const auth = createAuth(db, env.APP_URL) + const auth = createAuth(db, env.APP_URL, env.PUBLIC_API_URL) const enableTokens = Boolean(context.req.header('x-enable-tokens')) async function getSession() { diff --git a/packages/api/src/routes/user.ts b/packages/api/src/routes/user.ts index 1afe55975..9cd5a93bb 100644 --- a/packages/api/src/routes/user.ts +++ b/packages/api/src/routes/user.ts @@ -32,6 +32,7 @@ import { getCookie } from 'hono/cookie' import { parseJWT } from 'oslo/jwt' import { P, match } from 'ts-pattern' import { AuthProviderName } from '../auth/providers' +import { getCookieOptions } from '../auth' export function sanitizeUserIdInput({ ctx, @@ -229,9 +230,7 @@ const authorizationUrlHandler = } const secure = ctx.req?.url.startsWith('https:') ? 'Secure; ' : '' ctx.setCookie( - `${input.provider}_oauth_redirect=${ - input.redirectTo || '' - }; Path=/; ${secure}HttpOnly; SameSite=Lax` + `${input.provider}_oauth_redirect=${input.redirectTo || ''}; Path=/; ${getCookieOptions(ctx)}` ) return { redirectTo: url.toString() } }