diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index d67e7b562..6e39d0451 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -108,7 +108,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { tokenVersionId: tokenVersion.id, accessVersion: tokenVersion.accessVersion, organizationId: decodedToken.organizationId, - isMfaVerified: decodedToken.isMfaVerified + isMfaVerified: decodedToken.isMfaVerified, + mfaMethod: decodedToken.mfaMethod }, appCfg.AUTH_SECRET, { expiresIn: appCfg.JWT_AUTH_LIFETIME } diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 30d032e13..07f795779 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -15,7 +15,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { getLastMidnightDateISO } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; import { integrationAuthPubSchema } from "../sanitizedSchemas"; @@ -259,7 +259,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { message: "Membership role must be a valid slug" }) .optional(), - enforceMfa: z.boolean().optional() + enforceMfa: z.boolean().optional(), + selectedMfaMethod: z.nativeEnum(MfaMethod).optional() }), response: { 200: z.object({ diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 2eb92af06..a580af2f5 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -396,7 +396,8 @@ export const authLoginServiceFactory = ({ userAgent, ip: ipAddress, organizationId, - isMfaVerified: decodedToken.isMfaVerified + isMfaVerified: decodedToken.isMfaVerified, + mfaMethod: decodedToken.mfaMethod }); return { diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 10227f595..c1bf2b6fb 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -72,6 +72,7 @@ export type AuthModeRefreshJwtTokenPayload = { refreshVersion: number; organizationId?: string; isMfaVerified?: boolean; + mfaMethod?: MfaMethod; }; export type AuthModeProviderJwtTokenPayload = { diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 3d74f80b6..1d656c4d5 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -268,7 +268,7 @@ export const orgServiceFactory = ({ actorOrgId, actorAuthMethod, orgId, - data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa } + data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa, selectedMfaMethod } }: TUpdateOrgDTO) => { const appCfg = getConfig(); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); @@ -333,7 +333,8 @@ export const orgServiceFactory = ({ authEnforced, scimEnabled, defaultMembershipRole, - enforceMfa + enforceMfa, + selectedMfaMethod }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); return org; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 5b44eeea5..7d4a203de 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -1,6 +1,6 @@ import { TOrgPermission } from "@app/lib/types"; -import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type"; export type TUpdateOrgMembershipDTO = { userId: string; @@ -65,6 +65,7 @@ export type TUpdateOrgDTO = { scimEnabled: boolean; defaultMembershipRoleSlug: string; enforceMfa: boolean; + selectedMfaMethod: MfaMethod; }>; } & TOrgPermission; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index ad44d280a..4923177ba 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -91,7 +91,8 @@ export const useUpdateOrg = () => { slug, orgId, defaultMembershipRoleSlug, - enforceMfa + enforceMfa, + selectedMfaMethod }) => { return apiRequest.patch(`/api/v1/organization/${orgId}`, { name, @@ -99,7 +100,8 @@ export const useUpdateOrg = () => { scimEnabled, slug, defaultMembershipRoleSlug, - enforceMfa + enforceMfa, + selectedMfaMethod }); }, onSuccess: () => { diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index b644cb325..5fbbd1863 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -1,6 +1,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types"; import { IdentityMembershipOrg } from "@app/hooks/api/identities/types"; +import { MfaMethod } from "../auth/types"; + export type Organization = { id: string; name: string; @@ -12,6 +14,7 @@ export type Organization = { slug: string; defaultMembershipRole: string; enforceMfa: boolean; + selectedMfaMethod?: MfaMethod; }; export type UpdateOrgDTO = { @@ -22,6 +25,7 @@ export type UpdateOrgDTO = { slug?: string; defaultMembershipRoleSlug?: string; enforceMfa?: boolean; + selectedMfaMethod?: MfaMethod; }; export type BillingDetails = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index d4d60d274..d6164cba6 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -78,6 +78,7 @@ import { useLogoutUser, useSelectOrganization } from "@app/hooks/api"; +import { MfaMethod } from "@app/hooks/api/auth/types"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; import { Workspace } from "@app/hooks/api/types"; @@ -143,6 +144,7 @@ export const AppLayout = ({ children }: LayoutProps) => { const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const workspacesWithFaveProp = useMemo( @@ -214,12 +216,15 @@ export const AppLayout = ({ children }: LayoutProps) => { }; const changeOrg = async (orgId: string) => { - const { token, isMfaEnabled } = await selectOrganization({ + const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId: orgId }); if (isMfaEnabled) { SecurityClient.setMfaToken(token); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } toggleShowMfa.on(); setMfaSuccessCallback(() => () => changeOrg(orgId)); return; @@ -365,6 +370,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
Enforce members to authenticate with MFA in order to access the organization
+ {currentOrg?.enforceMfa && ( +