diff --git a/apps/remix/server/router.ts b/apps/remix/server/router.ts index 12ac294ce..037abcaf6 100644 --- a/apps/remix/server/router.ts +++ b/apps/remix/server/router.ts @@ -12,6 +12,8 @@ import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app'; import { jobsClient } from '@documenso/lib/jobs/client'; import { LicenseClient } from '@documenso/lib/server-only/license/license-client'; import { TelemetryClient } from '@documenso/lib/server-only/telemetry/telemetry-client'; +import { migrateDeletedAccountServiceAccount } from '@documenso/lib/server-only/user/service-accounts/deleted-account'; +import { migrateLegacyServiceAccount } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account'; import { getIpAddress } from '@documenso/lib/universal/get-ip-address'; import { env } from '@documenso/lib/utils/env'; import { logger } from '@documenso/lib/utils/logger'; @@ -144,4 +146,7 @@ if (env('NODE_ENV') !== 'development') { // Start license client to verify license on startup. void LicenseClient.start(); +void migrateDeletedAccountServiceAccount(); +void migrateLegacyServiceAccount(); + export default app; diff --git a/packages/auth/server/lib/utils/handle-oauth-callback-url.ts b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts index 92273fdeb..1cc404b04 100644 --- a/packages/auth/server/lib/utils/handle-oauth-callback-url.ts +++ b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts @@ -5,6 +5,8 @@ import { deleteCookie } from 'hono/cookie'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; +import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account'; +import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account'; import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to'; import { prisma } from '@documenso/prisma'; @@ -26,6 +28,13 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } = await validateOauth({ c, clientOptions }); + if ( + email.toLowerCase() === legacyServiceAccountEmail() || + email.toLowerCase() === deletedServiceAccountEmail() + ) { + return c.text('FORBIDDEN', 403); + } + // Find the account if possible. const existingAccount = await prisma.account.findFirst({ where: { diff --git a/packages/auth/server/routes/email-password.ts b/packages/auth/server/routes/email-password.ts index fbb2bad9e..7d7a9b2a9 100644 --- a/packages/auth/server/routes/email-password.ts +++ b/packages/auth/server/routes/email-password.ts @@ -17,7 +17,10 @@ import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-code import { createUser } from '@documenso/lib/server-only/user/create-user'; import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; import { getMostRecentEmailVerificationToken } from '@documenso/lib/server-only/user/get-most-recent-email-verification-token'; +import { getUserByResetToken } from '@documenso/lib/server-only/user/get-user-by-reset-token'; import { resetPassword } from '@documenso/lib/server-only/user/reset-password'; +import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account'; +import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account'; import { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { verifyEmail } from '@documenso/lib/server-only/user/verify-email'; import { env } from '@documenso/lib/utils/env'; @@ -57,6 +60,13 @@ export const emailPasswordRoute = new Hono() }); } + if ( + email.toLowerCase() === legacyServiceAccountEmail() || + email.toLowerCase() === deletedServiceAccountEmail() + ) { + return c.text('FORBIDDEN', 403); + } + const user = await prisma.user.findFirst({ where: { email: email.toLowerCase(), @@ -241,6 +251,13 @@ export const emailPasswordRoute = new Hono() .post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => { const { email } = c.req.valid('json'); + if ( + email.toLowerCase() === legacyServiceAccountEmail() || + email.toLowerCase() === deletedServiceAccountEmail() + ) { + return c.text('FORBIDDEN', 403); + } + await forgotPassword({ email, }); @@ -253,6 +270,15 @@ export const emailPasswordRoute = new Hono() .post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => { const { token, password } = c.req.valid('json'); + const user = await getUserByResetToken({ token }); + + if ( + user.email.toLowerCase() === legacyServiceAccountEmail() || + user.email.toLowerCase() === deletedServiceAccountEmail() + ) { + return c.text('FORBIDDEN', 403); + } + const requestMetadata = c.get('requestMetadata'); const { userId } = await resetPassword({ diff --git a/packages/auth/server/routes/passkey.ts b/packages/auth/server/routes/passkey.ts index 1bb3e16de..b9f057be5 100644 --- a/packages/auth/server/routes/passkey.ts +++ b/packages/auth/server/routes/passkey.ts @@ -5,6 +5,8 @@ import { isoBase64URL } from '@simplewebauthn/server/helpers'; import { Hono } from 'hono'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account'; +import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account'; import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn'; import { ZAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn'; import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator'; @@ -74,6 +76,13 @@ export const passkeyRoute = new Hono() const user = passkey.user; + if ( + user.email.toLowerCase() === legacyServiceAccountEmail() || + user.email.toLowerCase() === deletedServiceAccountEmail() + ) { + return c.text('FORBIDDEN', 403); + } + const { rpId, origin } = getAuthenticatorOptions(); const verification = await verifyAuthenticationResponse({ diff --git a/packages/lib/constants/email.ts b/packages/lib/constants/email.ts index 1d6a7dc07..a6742c5a8 100644 --- a/packages/lib/constants/email.ts +++ b/packages/lib/constants/email.ts @@ -8,8 +8,6 @@ export const DOCUMENSO_INTERNAL_EMAIL = { address: FROM_ADDRESS, }; -export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com'; - export const EMAIL_VERIFICATION_STATE = { NOT_FOUND: 'NOT_FOUND', VERIFIED: 'VERIFIED', diff --git a/packages/lib/server-only/user/get-user-by-reset-token.ts b/packages/lib/server-only/user/get-user-by-reset-token.ts new file mode 100644 index 000000000..c95965bde --- /dev/null +++ b/packages/lib/server-only/user/get-user-by-reset-token.ts @@ -0,0 +1,24 @@ +import { prisma } from '@documenso/prisma'; + +import { AppError, AppErrorCode } from '../../errors/app-error'; + +export interface GetUserByResetTokenOptions { + token: string; +} + +export const getUserByResetToken = async ({ token }: GetUserByResetTokenOptions) => { + const result = await prisma.passwordResetToken.findFirst({ + where: { + token, + }, + include: { + user: true, + }, + }); + + if (!result || !result.user) { + throw new AppError(AppErrorCode.NOT_FOUND); + } + + return result.user; +}; diff --git a/packages/lib/server-only/user/service-accounts/deleted-account.ts b/packages/lib/server-only/user/service-accounts/deleted-account.ts index 2c54d2f8c..38c2b8fcb 100644 --- a/packages/lib/server-only/user/service-accounts/deleted-account.ts +++ b/packages/lib/server-only/user/service-accounts/deleted-account.ts @@ -1,9 +1,27 @@ import { prisma } from '@documenso/prisma'; +const LEGACY_DELETED_ACCOUNT_EMAIL = 'deleted-account@documenso.com'; + +export const deletedServiceAccountEmail = () => { + try { + // eslint-disable-next-line turbo/no-undeclared-env-vars + if (process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL) { + // eslint-disable-next-line turbo/no-undeclared-env-vars + return process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL; + } + + const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'); + + return `deleted-account@${hostname}`; + } catch (error) { + return LEGACY_DELETED_ACCOUNT_EMAIL; + } +}; + export const deletedAccountServiceAccount = async () => { const serviceAccount = await prisma.user.findFirst({ where: { - email: 'deleted-account@documenso.com', + email: deletedServiceAccountEmail(), }, select: { id: true, @@ -29,3 +47,20 @@ export const deletedAccountServiceAccount = async () => { return serviceAccount; }; + +export const migrateDeletedAccountServiceAccount = async () => { + if (deletedServiceAccountEmail() !== LEGACY_DELETED_ACCOUNT_EMAIL) { + console.log( + `Migrating deleted account service account to new email: ${deletedServiceAccountEmail()}`, + ); + + await prisma.user.updateMany({ + where: { + email: LEGACY_DELETED_ACCOUNT_EMAIL, + }, + data: { + email: deletedServiceAccountEmail(), + }, + }); + } +}; diff --git a/packages/lib/server-only/user/service-accounts/legacy-service-account.ts b/packages/lib/server-only/user/service-accounts/legacy-service-account.ts new file mode 100644 index 000000000..3299c2a47 --- /dev/null +++ b/packages/lib/server-only/user/service-accounts/legacy-service-account.ts @@ -0,0 +1,34 @@ +import { prisma } from '@documenso/prisma'; + +const LEGACY_SERVICE_ACCOUNT_EMAIL = 'serviceaccount@documenso.com'; + +export const legacyServiceAccountEmail = () => { + try { + // eslint-disable-next-line turbo/no-undeclared-env-vars + if (process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL) { + // eslint-disable-next-line turbo/no-undeclared-env-vars + return process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL; + } + + const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'); + + return `serviceaccount@${hostname}`; + } catch (error) { + return LEGACY_SERVICE_ACCOUNT_EMAIL; + } +}; + +export const migrateLegacyServiceAccount = async () => { + if (legacyServiceAccountEmail() !== LEGACY_SERVICE_ACCOUNT_EMAIL) { + console.log(`Migrating legacy service account to new email: ${legacyServiceAccountEmail()}`); + + await prisma.user.updateMany({ + where: { + email: LEGACY_SERVICE_ACCOUNT_EMAIL, + }, + data: { + email: legacyServiceAccountEmail(), + }, + }); + } +};