diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_index.tsx index 0683f5066..4911167df 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_index.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_index.tsx @@ -5,8 +5,9 @@ import { formatDocumentsPath } from '@documenso/lib/utils/teams'; export function loader() { const { currentTeam } = getLoaderSession(); + if (!currentTeam) { - throw redirect('/documents'); + throw redirect('/settings/teams'); } throw redirect(formatDocumentsPath(currentTeam.url)); diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx index 80023bf3c..21617a6ed 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/_layout.tsx @@ -19,7 +19,7 @@ export const loader = () => { const { currentTeam } = getLoaderSession(); if (!currentTeam) { - throw redirect('/documents'); + throw redirect('/settings/teams'); } const trpcHeaders = { diff --git a/apps/remix/package.json b/apps/remix/package.json index 160983a20..bc99f8c40 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -97,7 +97,6 @@ "typescript": "5.6.2", "vite": "^6.1.0", "vite-plugin-babel-macros": "^1.0.6", - "vite-plugin-checker": "^0.8.0", "vite-tsconfig-paths": "^5.1.4" } -} \ No newline at end of file +} diff --git a/apps/remix/server/middleware.ts b/apps/remix/server/middleware.ts index d34d451e9..5d94b6eae 100644 --- a/apps/remix/server/middleware.ts +++ b/apps/remix/server/middleware.ts @@ -1,7 +1,6 @@ import type { Context, Next } from 'hono'; import { getCookie } from 'hono/cookie'; -import { setCsrfCookie } from '@documenso/auth/server/lib/session/session-cookies'; import { AppLogger } from '@documenso/lib/utils/debugger'; const logger = new AppLogger('Middleware'); @@ -32,14 +31,6 @@ export const appMiddleware = async (c: Context, next: Next) => { const referrerUrl = referrer ? new URL(referrer) : null; const referrerPathname = referrerUrl ? referrerUrl.pathname : null; - // Set csrf token if not set. - const csrfToken = getCookie(c, 'csrfToken'); - - // Todo: Currently not working. - if (!csrfToken) { - await setCsrfCookie(c); - } - // // Whether to reset the preferred team url cookie if the user accesses a non team page from a team page. // const resetPreferredTeamUrl = // referrerPathname && @@ -59,24 +50,6 @@ export const appMiddleware = async (c: Context, next: Next) => { // return c.redirect(redirectUrl); // } - // // Redirect `/t` to `/settings/teams`. - // if (path === '/t' || path === '/t/') { - // logger.log('Redirecting to /settings/teams'); - - // const redirectUrl = new URL('/settings/teams', req.url); - // return c.redirect(redirectUrl); - // } - - // // Redirect `/t/` to `/t//documents`. - // if (TEAM_URL_ROOT_REGEX.test(path)) { - // logger.log('Redirecting team documents'); - - // const redirectUrl = new URL(`${path}/documents`, req.url); - // setCookie(c, 'preferred-team-url', path.replace('/t/', '')); - - // return c.redirect(redirectUrl); - // } - // // Set the preferred team url cookie if user accesses a team page. // if (path.startsWith('/t/')) { // setCookie(c, 'preferred-team-url', path.split('/')[2]); @@ -90,6 +63,4 @@ export const appMiddleware = async (c: Context, next: Next) => { // deleteCookie(c, 'preferred-team-url'); // return next(); // } - - return next(); }; diff --git a/apps/remix/server/router.ts b/apps/remix/server/router.ts index c904f6d6b..45b1f9c44 100644 --- a/apps/remix/server/router.ts +++ b/apps/remix/server/router.ts @@ -9,7 +9,6 @@ import { openApiDocument } from '@documenso/trpc/server/open-api'; import { filesRoute } from './api/files'; import { type AppContext, appContext } from './context'; -import { appMiddleware } from './middleware'; import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api'; import { reactRouterTrpcServer } from './trpc/hono-trpc-remix'; @@ -30,7 +29,7 @@ app.use(appContext); /** * Middleware for initial page loads. */ -app.use('*', appMiddleware); +// app.use('*', appMiddleware); // Auth server. app.route('/api/auth', auth); diff --git a/package-lock.json b/package-lock.json index 813afeafe..d3c173cf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -179,7 +179,6 @@ "typescript": "5.6.2", "vite": "^6.1.0", "vite-plugin-babel-macros": "^1.0.6", - "vite-plugin-checker": "^0.8.0", "vite-tsconfig-paths": "^5.1.4" } }, @@ -849,16 +848,6 @@ "win32" ] }, - "apps/remix/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "apps/remix/node_modules/esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -900,35 +889,6 @@ "@esbuild/win32-x64": "0.24.2" } }, - "apps/remix/node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "apps/remix/node_modules/rollup": { "version": "4.34.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.5.tgz", @@ -968,88 +928,6 @@ "fsevents": "~2.3.2" } }, - "apps/remix/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "apps/remix/node_modules/vite-plugin-checker": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz", - "integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "ansi-escapes": "^4.3.0", - "chalk": "^4.1.1", - "chokidar": "^3.5.1", - "commander": "^8.0.0", - "fast-glob": "^3.2.7", - "fs-extra": "^11.1.0", - "npm-run-path": "^4.0.1", - "strip-ansi": "^6.0.0", - "tiny-invariant": "^1.1.0", - "vscode-languageclient": "^7.0.0", - "vscode-languageserver": "^7.0.0", - "vscode-languageserver-textdocument": "^1.0.1", - "vscode-uri": "^3.0.2" - }, - "engines": { - "node": ">=14.16" - }, - "peerDependencies": { - "@biomejs/biome": ">=1.7", - "eslint": ">=7", - "meow": "^9.0.0", - "optionator": "^0.9.1", - "stylelint": ">=13", - "typescript": "*", - "vite": ">=2.0.0", - "vls": "*", - "vti": "*", - "vue-tsc": "~2.1.6" - }, - "peerDependenciesMeta": { - "@biomejs/biome": { - "optional": true - }, - "eslint": { - "optional": true - }, - "meow": { - "optional": true - }, - "optionator": { - "optional": true - }, - "stylelint": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vls": { - "optional": true - }, - "vti": { - "optional": true - }, - "vue-tsc": { - "optional": true - } - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -40093,69 +39971,6 @@ "@esbuild/win32-x64": "0.24.2" } }, - "node_modules/vscode-jsonrpc": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", - "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0 || >=10.0.0" - } - }, - "node_modules/vscode-languageclient": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", - "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.4", - "semver": "^7.3.4", - "vscode-languageserver-protocol": "3.16.0" - }, - "engines": { - "vscode": "^1.52.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", - "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", - "dev": true, - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.16.0" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", - "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", - "dev": true, - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "6.0.0", - "vscode-languageserver-types": "3.16.0" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", - "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", - "dev": true, - "license": "MIT" - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -40168,13 +39983,6 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", "license": "MIT" }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, "node_modules/wait-on": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.2.tgz", diff --git a/packages/auth/client/index.ts b/packages/auth/client/index.ts index d1eb12122..236aed72e 100644 --- a/packages/auth/client/index.ts +++ b/packages/auth/client/index.ts @@ -53,8 +53,16 @@ export class AuthClient { } public emailPassword = { - signIn: async (data: TEmailPasswordSignin) => { - const response = await this.client['email-password'].authorize.$post({ json: data }); + signIn: async (data: Omit) => { + const { csrfToken } = await this.client.csrf.$get().then(async (res) => res.json()); + + const response = await this.client['email-password'].authorize.$post({ + json: { + ...data, + csrfToken, + }, + }); + await this.handleError(response); handleSignInRedirect(data.redirectPath); diff --git a/packages/auth/server/index.ts b/packages/auth/server/index.ts index 70b1f8f90..3c784bc89 100644 --- a/packages/auth/server/index.ts +++ b/packages/auth/server/index.ts @@ -2,9 +2,11 @@ import { Hono } from 'hono'; import { HTTPException } from 'hono/http-exception'; import type { ContentfulStatusCode } from 'hono/utils/http-status'; +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { setCsrfCookie } from './lib/session/session-cookies'; import { emailPasswordRoute } from './routes/email-password'; import { googleRoute } from './routes/google'; import { passkeyRoute } from './routes/passkey'; @@ -16,8 +18,28 @@ import type { HonoAuthContext } from './types/context'; export const auth = new Hono() .use(async (c, next) => { c.set('requestMetadata', extractRequestMetadata(c.req.raw)); + + // Todo: Maybe use auth URL. + const validOrigin = new URL(NEXT_PUBLIC_WEBAPP_URL()).origin; + const headerOrigin = c.req.header('Origin'); + + if (headerOrigin && headerOrigin !== validOrigin) { + return c.json( + { + message: 'Forbidden', + statusCode: 403, + }, + 403, + ); + } + await next(); }) + .get('/csrf', async (c) => { + const csrfToken = await setCsrfCookie(c); + + return c.json({ csrfToken }); + }) .route('/', sessionRoute) .route('/', signOutRoute) .route('/email-password', emailPasswordRoute) diff --git a/packages/auth/server/lib/session/session-cookies.ts b/packages/auth/server/lib/session/session-cookies.ts index 69a84faf9..007c219bc 100644 --- a/packages/auth/server/lib/session/session-cookies.ts +++ b/packages/auth/server/lib/session/session-cookies.ts @@ -6,6 +6,8 @@ import { useSecureCookies } from '@documenso/lib/constants/auth'; import { appLog } from '@documenso/lib/utils/debugger'; import { env } from '@documenso/lib/utils/env'; +import { generateSessionToken } from './session'; + export const sessionCookieName = 'sessionId'; const getAuthSecret = () => { @@ -30,7 +32,7 @@ const getAuthDomain = () => { export const sessionCookieOptions = { httpOnly: true, path: '/', - sameSite: useSecureCookies ? 'none' : 'lax', + sameSite: useSecureCookies ? 'none' : 'lax', // Todo: This feels wrong? secure: useSecureCookies, domain: getAuthDomain(), // Todo: Max age for specific auth cookies. @@ -89,3 +91,23 @@ export const setSessionCookie = async (c: Context, sessionToken: string) => { export const deleteSessionCookie = (c: Context) => { deleteCookie(c, sessionCookieName, sessionCookieOptions); }; + +export const getCsrfCookie = async (c: Context) => { + const csrfToken = await getSignedCookie(c, getAuthSecret(), 'csrfToken'); + + return csrfToken || null; +}; + +export const setCsrfCookie = async (c: Context) => { + const csrfToken = generateSessionToken(); + + await setSignedCookie(c, 'csrfToken', csrfToken, getAuthSecret(), { + ...sessionCookieOptions, + + // Explicity set to undefined for session lived cookie. + expires: undefined, + maxAge: undefined, + }); + + return csrfToken; +}; diff --git a/packages/auth/server/routes/email-password.ts b/packages/auth/server/routes/email-password.ts index 5112cb809..4cc06452f 100644 --- a/packages/auth/server/routes/email-password.ts +++ b/packages/auth/server/routes/email-password.ts @@ -25,6 +25,7 @@ import { prisma } from '@documenso/prisma'; import { UserSecurityAuditLogType } from '@documenso/prisma/client'; import { AuthenticationErrorCode } from '../lib/errors/error-codes'; +import { getCsrfCookie } from '../lib/session/session-cookies'; import { onAuthorize } from '../lib/utils/authorizer'; import { getRequiredSession, getSession } from '../lib/utils/get-session'; import type { HonoAuthContext } from '../types/context'; @@ -45,7 +46,16 @@ export const emailPasswordRoute = new Hono() .post('/authorize', sValidator('json', ZSignInSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); - const { email, password, totpCode, backupCode } = c.req.valid('json'); + const { email, password, totpCode, backupCode, csrfToken } = c.req.valid('json'); + + const csrfCookieToken = await getCsrfCookie(c); + + // Todo: Add logging here. + if (csrfToken !== csrfCookieToken || !csrfCookieToken) { + throw new AppError(AuthenticationErrorCode.InvalidRequest, { + message: 'Invalid CSRF token', + }); + } const user = await prisma.user.findFirst({ where: { diff --git a/packages/auth/server/types/email-password.ts b/packages/auth/server/types/email-password.ts index 0ef7df2f6..aeaae5a02 100644 --- a/packages/auth/server/types/email-password.ts +++ b/packages/auth/server/types/email-password.ts @@ -10,6 +10,7 @@ export const ZSignInSchema = z.object({ password: ZCurrentPasswordSchema, totpCode: z.string().trim().optional(), backupCode: z.string().trim().optional(), + csrfToken: z.string().trim(), }); export type TSignInSchema = z.infer;