diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 1cc863168..9d239a405 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -18,6 +18,7 @@ export type TAuthMode = user: TUsers; orgId: string; authMethod: AuthMethod; + isMfaVerified?: boolean; } | { authMode: AuthMode.API_KEY; @@ -121,7 +122,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { tokenVersionId, actor, orgId: orgId as string, - authMethod: token.authMethod + authMethod: token.authMethod, + isMfaVerified: token.isMfaVerified }; break; } diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 61bc910a6..d67e7b562 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -107,7 +107,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { userId: decodedToken.userId, tokenVersionId: tokenVersion.id, accessVersion: tokenVersion.accessVersion, - organizationId: decodedToken.organizationId + organizationId: decodedToken.organizationId, + isMfaVerified: decodedToken.isMfaVerified }, appCfg.AUTH_SECRET, { expiresIn: appCfg.JWT_AUTH_LIFETIME } diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index a6c3a5bf6..39f3162d1 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -99,13 +99,15 @@ export const authLoginServiceFactory = ({ ip, userAgent, organizationId, - authMethod + authMethod, + isMfaVerified }: { user: TUsers; ip: string; userAgent: string; organizationId?: string; authMethod: AuthMethod; + isMfaVerified?: boolean; }) => { const cfg = getConfig(); await updateUserDeviceSession(user, ip, userAgent); @@ -123,7 +125,8 @@ export const authLoginServiceFactory = ({ userId: user.id, tokenVersionId: tokenSession.id, accessVersion: tokenSession.accessVersion, - organizationId + organizationId, + isMfaVerified }, cfg.AUTH_SECRET, { expiresIn: cfg.JWT_AUTH_LIFETIME } @@ -136,7 +139,8 @@ export const authLoginServiceFactory = ({ userId: user.id, tokenVersionId: tokenSession.id, refreshVersion: tokenSession.refreshVersion, - organizationId + organizationId, + isMfaVerified }, cfg.AUTH_SECRET, { expiresIn: cfg.JWT_REFRESH_LIFETIME } @@ -350,7 +354,7 @@ export const authLoginServiceFactory = ({ } // send multi factor auth token if they it enabled - if (user.isMfaEnabled && user.email) { + if (user.isMfaEnabled && user.email && !decodedToken.isMfaVerified) { enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd); const mfaToken = jwt.sign( @@ -378,7 +382,8 @@ export const authLoginServiceFactory = ({ user, userAgent, ip: ipAddress, - organizationId + organizationId, + isMfaVerified: decodedToken.isMfaVerified }); return { @@ -507,7 +512,8 @@ export const authLoginServiceFactory = ({ ip, userAgent, organizationId: orgId, - authMethod: decodedToken.authMethod + authMethod: decodedToken.authMethod, + isMfaVerified: true }); return { token, user: userEnc }; diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index 87522a803..44b775945 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -52,6 +52,7 @@ export type AuthModeJwtTokenPayload = { tokenVersionId: string; accessVersion: number; organizationId?: string; + isMfaVerified?: boolean; }; export type AuthModeMfaJwtTokenPayload = { @@ -69,6 +70,7 @@ export type AuthModeRefreshJwtTokenPayload = { tokenVersionId: string; refreshVersion: number; organizationId?: string; + isMfaVerified?: boolean; }; export type AuthModeProviderJwtTokenPayload = { diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index 5b9c0f72c..319a6d6df 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Head from "next/head"; import Image from "next/image"; @@ -12,15 +12,18 @@ import jwt_decode from "jwt-decode"; import { createNotification } from "@app/components/notifications"; import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin"; +import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Spinner } from "@app/components/v2"; import { SessionStorageKeys } from "@app/const"; import { useUser } from "@app/context"; +import { useToggle } from "@app/hooks"; import { useGetOrganizations, useLogoutUser, useSelectOrganization } from "@app/hooks/api"; import { UserAgentType } from "@app/hooks/api/auth/types"; import { Organization } from "@app/hooks/api/types"; import { AuthMethod } from "@app/hooks/api/users/types"; import { getAuthToken, isLoggedIn } from "@app/reactQuery"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; +import { Mfa } from "@app/views/Login/Mfa"; const LoadingScreen = () => { return ( @@ -37,7 +40,10 @@ export default function LoginPage() { const organizations = useGetOrganizations(); const selectOrg = useSelectOrganization(); + const { user, isLoading: userLoading } = useUser(); + const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const queryParams = new URLSearchParams(window.location.search); const callbackPort = queryParams.get("callback_port"); @@ -77,11 +83,19 @@ export default function LoginPage() { return; } - const { token } = await selectOrg.mutateAsync({ + const { token, isMfaEnabled } = await selectOrg.mutateAsync({ organizationId: organization.id, userAgent: callbackPort ? UserAgentType.CLI : undefined }); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + toggleShowMfa.on(); + + setMfaSuccessCallback(() => () => handleSelectOrganization(organization)); + return; + } + if (callbackPort) { const privateKey = localStorage.getItem("PRIVATE_KEY"); @@ -178,56 +192,57 @@ export default function LoginPage() { -
{t("mfa.step2-message")}
+{user.email}
+{t("signup.step2-spam-alert")}
+