diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index dc4c06c4a..eee3db467 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -788,6 +788,7 @@ export const registerRoutes = async ( smtpService, authDAL, userDAL, + orgMembershipDAL, totpConfigDAL }); diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index efc8b3cc0..21a51ef3f 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -6,6 +6,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; @@ -22,6 +23,7 @@ import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; userDAL: TUserDALFactory; + orgMembershipDAL: Pick; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; totpConfigDAL: Pick; @@ -31,6 +33,7 @@ export type TAuthPasswordFactory = ReturnType; export const authPaswordServiceFactory = ({ authDAL, userDAL, + orgMembershipDAL, tokenService, smtpService, totpConfigDAL @@ -47,21 +50,46 @@ export const authPaswordServiceFactory = ({ if (user && user.isAccepted) { const cfg = getConfig(); - const token = await tokenService.createTokenForUser({ - type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, - userId: user.id - }); - await smtpService.sendMail({ - template: SmtpTemplates.ResetPassword, - recipients: [email], - subjectLine: "Infisical password reset", - substitutions: { + const hasEmailAuth = user.authMethods?.includes(AuthMethod.EMAIL); + + if (!hasEmailAuth) { + const orgMemberships = await orgMembershipDAL.find({ userId: user.id }); + const lastLoginMethod = + orgMemberships + .filter((membership) => membership.lastLoginAuthMethod) + .sort((a, b) => (b.updatedAt || new Date(0)).getTime() - (a.updatedAt || new Date(0)).getTime())[0] + ?.lastLoginAuthMethod || null; + const substitutions = { email, - token, - callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" - } - }); + lastLoginMethod, + isCloud: cfg.isCloud, + siteUrl: cfg.SITE_URL || "" + }; + + await smtpService.sendMail({ + template: SmtpTemplates.OAuthPasswordReset, + recipients: [email], + subjectLine: "Password reset not available", + substitutions + }); + } else { + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, + userId: user.id + }); + + await smtpService.sendMail({ + template: SmtpTemplates.ResetPassword, + recipients: [email], + subjectLine: "Infisical password reset", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" + } + }); + } } }; diff --git a/backend/src/services/smtp/emails/OAuthPasswordResetTemplate.tsx b/backend/src/services/smtp/emails/OAuthPasswordResetTemplate.tsx new file mode 100644 index 000000000..97fc8d7e3 --- /dev/null +++ b/backend/src/services/smtp/emails/OAuthPasswordResetTemplate.tsx @@ -0,0 +1,80 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; + +interface OAuthPasswordResetTemplateProps extends Omit { + email: string; + lastLoginMethod?: string | null; + isCloud: boolean; +} + +export const OAuthPasswordResetTemplate = ({ + email, + lastLoginMethod, + isCloud, + siteUrl +}: OAuthPasswordResetTemplateProps) => { + const getAuthMethodDisplayName = (method: string) => { + return method + .split("-") + .map((word) => { + const upperWord = word.toUpperCase(); + if (["SAML", "LDAP", "OIDC", "SSO"].includes(upperWord)) { + return upperWord; + } + return word.charAt(0).toUpperCase() + word.slice(1); + }) + .join(" "); + }; + + const getAuthMethodMessage = () => { + if (lastLoginMethod) { + const displayName = getAuthMethodDisplayName(lastLoginMethod); + return `Please continue by signing in with ${displayName}.`; + } + return "Please continue using the same authentication method you previously used to sign in (e.g., SSO, SAML, OAuth, or another configured provider)."; + }; + return ( + + + Password Reset Not Available + +
+ + Password reset is not available for this account. + + + A password reset was requested for your Infisical account ({email}), but password login has not been enabled + for your account. + + {getAuthMethodMessage()} + + If you did not initiate this request, please contact{" "} + {isCloud ? ( + <> + us immediately at support@infisical.com + + ) : ( + "your administrator immediately" + )} + . + +
+
+ ); +}; + +export default OAuthPasswordResetTemplate; + +OAuthPasswordResetTemplate.PreviewProps = { + email: "user@example.com", + lastLoginMethod: "github", + isCloud: true, + siteUrl: "https://infisical.com" +} as OAuthPasswordResetTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 71c338def..066744596 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -7,6 +7,7 @@ export * from "./ExternalImportStartedTemplate"; export * from "./ExternalImportSucceededTemplate"; export * from "./IntegrationSyncFailedTemplate"; export * from "./NewDeviceLoginTemplate"; +export * from "./OAuthPasswordResetTemplate"; export * from "./OrgAdminBreakglassAccessTemplate"; export * from "./OrgAdminProjectGrantAccessTemplate"; export * from "./OrganizationAssignmentTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 224f78265..d64582fe0 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -16,6 +16,7 @@ import { ExternalImportSucceededTemplate, IntegrationSyncFailedTemplate, NewDeviceLoginTemplate, + OAuthPasswordResetTemplate, OrgAdminBreakglassAccessTemplate, OrgAdminProjectGrantAccessTemplate, OrganizationAssignmentTemplate, @@ -63,6 +64,7 @@ export enum SmtpTemplates { NewDeviceJoin = "newDevice", OrgInvite = "organizationInvitation", OrgAssignment = "organizationAssignment", + OAuthPasswordReset = "oAuthPasswordReset", ResetPassword = "passwordReset", SetupPassword = "passwordSetup", SecretLeakIncident = "secretLeakIncident", @@ -121,6 +123,7 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.OrgAdminProjectDirectAccess]: OrgAdminProjectGrantAccessTemplate, [SmtpTemplates.ProjectAccessRequest]: ProjectAccessRequestTemplate, [SmtpTemplates.SecretApprovalRequestNeedsReview]: SecretApprovalRequestNeedsReviewTemplate, + [SmtpTemplates.OAuthPasswordReset]: OAuthPasswordResetTemplate, [SmtpTemplates.ResetPassword]: PasswordResetTemplate, [SmtpTemplates.SetupPassword]: PasswordSetupTemplate, [SmtpTemplates.PkiExpirationAlert]: PkiExpirationAlertTemplate,