misc: added org mfa settings update and other fixes

This commit is contained in:
Sheen Capadngan
2024-11-14 01:16:15 +08:00
parent 6bdbac4750
commit 44ba31a743
13 changed files with 93 additions and 15 deletions
+2 -1
View File
@@ -108,7 +108,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
tokenVersionId: tokenVersion.id, tokenVersionId: tokenVersion.id,
accessVersion: tokenVersion.accessVersion, accessVersion: tokenVersion.accessVersion,
organizationId: decodedToken.organizationId, organizationId: decodedToken.organizationId,
isMfaVerified: decodedToken.isMfaVerified isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
}, },
appCfg.AUTH_SECRET, appCfg.AUTH_SECRET,
{ expiresIn: appCfg.JWT_AUTH_LIFETIME } { expiresIn: appCfg.JWT_AUTH_LIFETIME }
@@ -15,7 +15,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn"; import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; 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"; import { integrationAuthPubSchema } from "../sanitizedSchemas";
@@ -259,7 +259,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
message: "Membership role must be a valid slug" message: "Membership role must be a valid slug"
}) })
.optional(), .optional(),
enforceMfa: z.boolean().optional() enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -396,7 +396,8 @@ export const authLoginServiceFactory = ({
userAgent, userAgent,
ip: ipAddress, ip: ipAddress,
organizationId, organizationId,
isMfaVerified: decodedToken.isMfaVerified isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
}); });
return { return {
+1
View File
@@ -72,6 +72,7 @@ export type AuthModeRefreshJwtTokenPayload = {
refreshVersion: number; refreshVersion: number;
organizationId?: string; organizationId?: string;
isMfaVerified?: boolean; isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
}; };
export type AuthModeProviderJwtTokenPayload = { export type AuthModeProviderJwtTokenPayload = {
+3 -2
View File
@@ -268,7 +268,7 @@ export const orgServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
orgId, orgId,
data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa } data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa, selectedMfaMethod }
}: TUpdateOrgDTO) => { }: TUpdateOrgDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
@@ -333,7 +333,8 @@ export const orgServiceFactory = ({
authEnforced, authEnforced,
scimEnabled, scimEnabled,
defaultMembershipRole, defaultMembershipRole,
enforceMfa enforceMfa,
selectedMfaMethod
}); });
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org; return org;
+2 -1
View File
@@ -1,6 +1,6 @@
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type";
export type TUpdateOrgMembershipDTO = { export type TUpdateOrgMembershipDTO = {
userId: string; userId: string;
@@ -65,6 +65,7 @@ export type TUpdateOrgDTO = {
scimEnabled: boolean; scimEnabled: boolean;
defaultMembershipRoleSlug: string; defaultMembershipRoleSlug: string;
enforceMfa: boolean; enforceMfa: boolean;
selectedMfaMethod: MfaMethod;
}>; }>;
} & TOrgPermission; } & TOrgPermission;
@@ -91,7 +91,8 @@ export const useUpdateOrg = () => {
slug, slug,
orgId, orgId,
defaultMembershipRoleSlug, defaultMembershipRoleSlug,
enforceMfa enforceMfa,
selectedMfaMethod
}) => { }) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, { return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name, name,
@@ -99,7 +100,8 @@ export const useUpdateOrg = () => {
scimEnabled, scimEnabled,
slug, slug,
defaultMembershipRoleSlug, defaultMembershipRoleSlug,
enforceMfa enforceMfa,
selectedMfaMethod
}); });
}, },
onSuccess: () => { onSuccess: () => {
@@ -1,6 +1,8 @@
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types"; import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
import { MfaMethod } from "../auth/types";
export type Organization = { export type Organization = {
id: string; id: string;
name: string; name: string;
@@ -12,6 +14,7 @@ export type Organization = {
slug: string; slug: string;
defaultMembershipRole: string; defaultMembershipRole: string;
enforceMfa: boolean; enforceMfa: boolean;
selectedMfaMethod?: MfaMethod;
}; };
export type UpdateOrgDTO = { export type UpdateOrgDTO = {
@@ -22,6 +25,7 @@ export type UpdateOrgDTO = {
slug?: string; slug?: string;
defaultMembershipRoleSlug?: string; defaultMembershipRoleSlug?: string;
enforceMfa?: boolean; enforceMfa?: boolean;
selectedMfaMethod?: MfaMethod;
}; };
export type BillingDetails = { export type BillingDetails = {
+7 -1
View File
@@ -78,6 +78,7 @@ import {
useLogoutUser, useLogoutUser,
useSelectOrganization useSelectOrganization
} from "@app/hooks/api"; } from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { Workspace } from "@app/hooks/api/types"; import { Workspace } from "@app/hooks/api/types";
@@ -143,6 +144,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const workspacesWithFaveProp = useMemo( const workspacesWithFaveProp = useMemo(
@@ -214,12 +216,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
}; };
const changeOrg = async (orgId: string) => { const changeOrg = async (orgId: string) => {
const { token, isMfaEnabled } = await selectOrganization({ const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId: orgId organizationId: orgId
}); });
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on(); toggleShowMfa.on();
setMfaSuccessCallback(() => () => changeOrg(orgId)); setMfaSuccessCallback(() => () => changeOrg(orgId));
return; return;
@@ -365,6 +370,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700"> <div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa <Mfa
email={user.email as string} email={user.email as string}
method={requiredMfaMethod}
successCallback={mfaSuccessCallback} successCallback={mfaSuccessCallback}
closeMfa={() => toggleShowMfa.off()} closeMfa={() => toggleShowMfa.off()}
/> />
@@ -16,6 +16,7 @@ import { Button, Input, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const"; import { SessionStorageKeys } from "@app/const";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
@@ -36,6 +37,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: selectOrganization } = useSelectOrganization();
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange(); const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
@@ -66,10 +68,13 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
if (organizationId) { if (organizationId) {
const finishWithOrgWorkflow = async () => { const finishWithOrgWorkflow = async () => {
const { token, isMfaEnabled } = await selectOrganization({ organizationId }); const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on(); toggleShowMfa.on();
setMfaSuccessCallback(() => finishWithOrgWorkflow); setMfaSuccessCallback(() => finishWithOrgWorkflow);
return; return;
@@ -167,10 +172,15 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
if (organizationId) { if (organizationId) {
const finishWithOrgWorkflow = async () => { const finishWithOrgWorkflow = async () => {
const { token, isMfaEnabled } = await selectOrganization({ organizationId }); const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId
});
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on(); toggleShowMfa.on();
setMfaSuccessCallback(() => finishWithOrgWorkflow); setMfaSuccessCallback(() => finishWithOrgWorkflow);
return; return;
@@ -283,6 +293,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
<Mfa <Mfa
email={email} email={email}
successCallback={mfaSuccessCallback} successCallback={mfaSuccessCallback}
method={requiredMfaMethod}
closeMfa={() => toggleShowMfa.off()} closeMfa={() => toggleShowMfa.off()}
/> />
</div> </div>
@@ -1,6 +1,6 @@
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Switch, UpgradePlanModal } from "@app/components/v2"; import { FormControl, Select, SelectItem, Switch, UpgradePlanModal } from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -8,6 +8,7 @@ import {
useSubscription useSubscription
} from "@app/context"; } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api"; import { useUpdateOrg } from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
export const OrgGenericAuthSection = () => { export const OrgGenericAuthSection = () => {
@@ -43,6 +44,32 @@ export const OrgGenericAuthSection = () => {
} }
}; };
const handleUpdateSelectedMfa = async (selectedMfaMethod: MfaMethod) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.enforceMfa) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
selectedMfaMethod
});
createNotification({
text: "Successfully updated preferred MFA method",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: (err as { response: { data: { message: string } } }).response.data.message,
type: "error"
});
}
};
return ( return (
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6"> <div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div className="py-4"> <div className="py-4">
@@ -62,6 +89,22 @@ export const OrgGenericAuthSection = () => {
<p className="text-sm text-mineshaft-300"> <p className="text-sm text-mineshaft-300">
Enforce members to authenticate with MFA in order to access the organization Enforce members to authenticate with MFA in order to access the organization
</p> </p>
{currentOrg?.enforceMfa && (
<FormControl label="Preferred 2FA method" className="mt-3">
<Select
className="min-w-[20rem] border border-mineshaft-500"
onValueChange={handleUpdateSelectedMfa}
defaultValue={currentOrg.selectedMfaMethod ?? MfaMethod.EMAIL}
>
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
Email
</SelectItem>
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
Mobile Authenticator
</SelectItem>
</Select>
</FormControl>
)}
</div> </div>
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
@@ -162,7 +162,7 @@ export const MFASection = () => {
Delete Delete
</Button> </Button>
</div> </div>
{shouldShowRecoveryCodes && totpConfiguration.recoveryCodes.length && ( {shouldShowRecoveryCodes && totpConfiguration.recoveryCodes && (
<div className="mt-4 bg-mineshaft-600 p-4"> <div className="mt-4 bg-mineshaft-600 p-4">
{totpConfiguration.recoveryCodes.map((code) => ( {totpConfiguration.recoveryCodes.map((code) => (
<div key={code}>{code}</div> <div key={code}>{code}</div>
@@ -13,6 +13,7 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2"; import { Button, Input } from "@app/components/v2";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import ProjectService from "@app/services/ProjectService"; import ProjectService from "@app/services/ProjectService";
import { Mfa } from "@app/views/Login/Mfa"; import { Mfa } from "@app/views/Login/Mfa";
@@ -57,6 +58,7 @@ export const UserInfoSSOStep = ({
const [organizationNameError, setOrganizationNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false);
const [attributionSource, setAttributionSource] = useState(""); const [attributionSource, setAttributionSource] = useState("");
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: selectOrganization } = useSelectOrganization();
@@ -178,12 +180,15 @@ export const UserInfoSSOStep = ({
const completeSignupFlow = async () => { const completeSignupFlow = async () => {
try { try {
const { isMfaEnabled, token } = await selectOrganization({ const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
organizationId: orgId organizationId: orgId
}); });
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on(); toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow); setMfaSuccessCallback(() => completeSignupFlow);
return; return;
@@ -231,6 +236,7 @@ export const UserInfoSSOStep = ({
hideLogo hideLogo
email={username} email={username}
successCallback={mfaSuccessCallback} successCallback={mfaSuccessCallback}
method={requiredMfaMethod}
closeMfa={() => toggleShowMfa.off()} closeMfa={() => toggleShowMfa.off()}
/> />
); );