MfaAuthSettingsForm.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import { useEffect, useState } from 'react'
  5. import { SubmitHandler, useForm } from 'react-hook-form'
  6. import { toast } from 'sonner'
  7. import {
  8. Alert,
  9. AlertTitle,
  10. Button,
  11. Card,
  12. CardContent,
  13. CardFooter,
  14. Form,
  15. FormControl,
  16. FormField,
  17. FormInputGroupInput,
  18. Input,
  19. InputGroup,
  20. InputGroupAddon,
  21. InputGroupText,
  22. Select,
  23. SelectContent,
  24. SelectItem,
  25. SelectTrigger,
  26. SelectValue,
  27. Switch,
  28. WarningIcon,
  29. } from 'ui'
  30. import { GenericSkeletonLoader } from 'ui-patterns'
  31. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  32. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  33. import {
  34. PageSection,
  35. PageSectionContent,
  36. PageSectionMeta,
  37. PageSectionSummary,
  38. PageSectionTitle,
  39. } from 'ui-patterns/PageSection'
  40. import * as z from 'zod'
  41. import { TaxDisclaimer } from '@/components/interfaces/Billing/TaxDisclaimer'
  42. import AlertError from '@/components/ui/AlertError'
  43. import NoPermission from '@/components/ui/NoPermission'
  44. import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
  45. import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
  46. import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
  47. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  48. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  49. import { IS_PLATFORM } from '@/lib/constants'
  50. function determineMFAStatus(verifyEnabled: boolean, enrollEnabled: boolean) {
  51. return verifyEnabled ? (enrollEnabled ? 'Enabled' : 'Verify Enabled') : 'Disabled'
  52. }
  53. const MFAFactorSelectionOptions = [
  54. {
  55. label: 'Enabled',
  56. value: 'Enabled',
  57. },
  58. {
  59. label: 'Verify Enabled',
  60. value: 'Verify Enabled',
  61. },
  62. {
  63. label: 'Disabled',
  64. value: 'Disabled',
  65. },
  66. ]
  67. const MfaStatusToState = (status: (typeof MFAFactorSelectionOptions)[number]['value']) => {
  68. return status === 'Enabled'
  69. ? { verifyEnabled: true, enrollEnabled: true }
  70. : status === 'Verify Enabled'
  71. ? { verifyEnabled: true, enrollEnabled: false }
  72. : { verifyEnabled: false, enrollEnabled: false }
  73. }
  74. const totpSchema = z.object({
  75. MFA_TOTP: z.string().min(1, 'Required'),
  76. MFA_MAX_ENROLLED_FACTORS: z.preprocess(
  77. (val) => (val === '' || val == null ? undefined : val),
  78. z.coerce
  79. .number({ required_error: 'Required', invalid_type_error: 'Required' })
  80. .min(0, 'Must be a value 0 or larger')
  81. .max(30, 'Must be a value no greater than 30')
  82. ),
  83. })
  84. type TotpFormValues = z.infer<typeof totpSchema>
  85. const phoneSchema = z.object({
  86. MFA_PHONE: z.string().min(1, 'Required'),
  87. MFA_PHONE_OTP_LENGTH: z.preprocess(
  88. (val) => (val === '' || val == null ? undefined : val),
  89. z.coerce
  90. .number({ required_error: 'Required', invalid_type_error: 'Required' })
  91. .min(6, 'Must be a value 6 or larger')
  92. .max(30, 'must be a value no greater than 30')
  93. ),
  94. MFA_PHONE_TEMPLATE: z.string().min(1, 'Required'),
  95. })
  96. type PhoneFormValues = z.infer<typeof phoneSchema>
  97. const securitySchema = z.object({
  98. MFA_ALLOW_LOW_AAL: z.boolean({ required_error: 'Required' }),
  99. })
  100. type SecurityFormValues = z.infer<typeof securitySchema>
  101. export const MfaAuthSettingsForm = () => {
  102. const { ref: projectRef } = useParams()
  103. const {
  104. data: authConfig,
  105. error: authConfigError,
  106. isError,
  107. isPending: isLoading,
  108. } = useAuthConfigQuery({ projectRef })
  109. const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
  110. // Separate loading states for each form
  111. const [isUpdatingTotpForm, setIsUpdatingTotpForm] = useState(false)
  112. const [isUpdatingPhoneForm, setIsUpdatingPhoneForm] = useState(false)
  113. const [isUpdatingSecurityForm, setIsUpdatingSecurityForm] = useState(false)
  114. const [isConfirmationModalVisible, setIsConfirmationModalVisible] = useState(false)
  115. const { can: canReadConfig } = useAsyncCheckPermissions(
  116. PermissionAction.READ,
  117. 'custom_config_gotrue'
  118. )
  119. const { can: canUpdateConfig } = useAsyncCheckPermissions(
  120. PermissionAction.UPDATE,
  121. 'custom_config_gotrue'
  122. )
  123. const { hasAccess: hasAccessToMFAEntitlement, isLoading: isLoadingEntitlement } =
  124. useCheckEntitlements('auth.mfa_phone')
  125. const hasAccessToMFA = !IS_PLATFORM || hasAccessToMFAEntitlement
  126. const promptProPlanUpgrade = IS_PLATFORM && !hasAccessToMFAEntitlement
  127. const {
  128. hasAccess: hasAccessToEnhanceSecurityEntitlement,
  129. isLoading: isLoadingEntitlementEnhanceSecurity,
  130. } = useCheckEntitlements('auth.mfa_enhanced_security')
  131. const hasAccessToEnhanceSecurity = !IS_PLATFORM || hasAccessToEnhanceSecurityEntitlement
  132. const promptEnhancedSecurityUpgrade = IS_PLATFORM && !hasAccessToEnhanceSecurityEntitlement
  133. // For now, we support Twilio and Vonage. Twilio Verify is not supported and the remaining providers are community maintained.
  134. const sendSMSHookIsEnabled =
  135. authConfig?.HOOK_SEND_SMS_URI !== null && authConfig?.HOOK_SEND_SMS_ENABLED === true
  136. const hasValidMFAPhoneProvider = authConfig?.EXTERNAL_PHONE_ENABLED === true
  137. const hasValidMFAProvider = hasValidMFAPhoneProvider || sendSMSHookIsEnabled
  138. const totpForm = useForm<TotpFormValues>({
  139. resolver: zodResolver(totpSchema as any),
  140. defaultValues: {
  141. MFA_TOTP: 'Enabled',
  142. MFA_MAX_ENROLLED_FACTORS: 10,
  143. },
  144. })
  145. const { reset: resetTotpForm } = totpForm
  146. const phoneForm = useForm<PhoneFormValues>({
  147. resolver: zodResolver(phoneSchema as any),
  148. defaultValues: {
  149. MFA_PHONE: 'Disabled',
  150. MFA_PHONE_OTP_LENGTH: 6,
  151. MFA_PHONE_TEMPLATE: 'Your code is {{ .Code }}',
  152. },
  153. })
  154. const { reset: resetPhoneForm } = phoneForm
  155. const securityForm = useForm<SecurityFormValues>({
  156. resolver: zodResolver(securitySchema as any),
  157. defaultValues: {
  158. MFA_ALLOW_LOW_AAL: false,
  159. },
  160. })
  161. const { reset: resetSecurityForm } = securityForm
  162. useEffect(() => {
  163. if (authConfig) {
  164. if (!isUpdatingTotpForm) {
  165. resetTotpForm({
  166. MFA_TOTP:
  167. determineMFAStatus(
  168. authConfig?.MFA_TOTP_VERIFY_ENABLED ?? true,
  169. authConfig?.MFA_TOTP_ENROLL_ENABLED ?? true
  170. ) || 'Enabled',
  171. MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS ?? 10,
  172. })
  173. }
  174. if (!isUpdatingPhoneForm) {
  175. resetPhoneForm({
  176. MFA_PHONE:
  177. determineMFAStatus(
  178. authConfig?.MFA_PHONE_VERIFY_ENABLED || false,
  179. authConfig?.MFA_PHONE_ENROLL_ENABLED || false
  180. ) || 'Disabled',
  181. MFA_PHONE_OTP_LENGTH: authConfig?.MFA_PHONE_OTP_LENGTH || 6,
  182. MFA_PHONE_TEMPLATE: authConfig?.MFA_PHONE_TEMPLATE || 'Your code is {{ .Code }}',
  183. })
  184. }
  185. if (!isUpdatingSecurityForm) {
  186. resetSecurityForm({
  187. MFA_ALLOW_LOW_AAL: authConfig?.MFA_ALLOW_LOW_AAL ?? true,
  188. })
  189. }
  190. }
  191. }, [
  192. authConfig,
  193. isUpdatingTotpForm,
  194. isUpdatingPhoneForm,
  195. isUpdatingSecurityForm,
  196. resetTotpForm,
  197. resetPhoneForm,
  198. resetSecurityForm,
  199. ])
  200. const onSubmitTotpForm: SubmitHandler<TotpFormValues> = (values) => {
  201. const { verifyEnabled: MFA_TOTP_VERIFY_ENABLED, enrollEnabled: MFA_TOTP_ENROLL_ENABLED } =
  202. MfaStatusToState(values.MFA_TOTP)
  203. const payload = {
  204. MFA_MAX_ENROLLED_FACTORS: values.MFA_MAX_ENROLLED_FACTORS,
  205. MFA_TOTP_ENROLL_ENABLED,
  206. MFA_TOTP_VERIFY_ENABLED,
  207. }
  208. setIsUpdatingTotpForm(true)
  209. updateAuthConfig(
  210. { projectRef: projectRef!, config: payload },
  211. {
  212. onError: (error) => {
  213. toast.error(`Failed to update TOTP settings: ${error?.message}`)
  214. setIsUpdatingTotpForm(false)
  215. },
  216. onSuccess: () => {
  217. toast.success('Successfully updated TOTP settings')
  218. setIsUpdatingTotpForm(false)
  219. },
  220. }
  221. )
  222. }
  223. const onSubmitSecurityForm: SubmitHandler<SecurityFormValues> = (values) => {
  224. setIsUpdatingSecurityForm(true)
  225. updateAuthConfig(
  226. { projectRef: projectRef!, config: values },
  227. {
  228. onError: (error) => {
  229. toast.error(`Failed to update enhanced MFA security settings: ${error?.message}`)
  230. setIsUpdatingSecurityForm(false)
  231. },
  232. onSuccess: () => {
  233. toast.success('Successfully updated enhanced MFA security settings')
  234. setIsUpdatingSecurityForm(false)
  235. },
  236. }
  237. )
  238. }
  239. const onSubmitPhoneForm: SubmitHandler<PhoneFormValues> = (values) => {
  240. let payload: Record<string, string | number | boolean> = {
  241. MFA_PHONE_OTP_LENGTH: values.MFA_PHONE_OTP_LENGTH,
  242. MFA_PHONE_TEMPLATE: values.MFA_PHONE_TEMPLATE,
  243. }
  244. if (hasAccessToMFA) {
  245. const { verifyEnabled: MFA_PHONE_VERIFY_ENABLED, enrollEnabled: MFA_PHONE_ENROLL_ENABLED } =
  246. MfaStatusToState(values.MFA_PHONE)
  247. payload = {
  248. MFA_PHONE_OTP_LENGTH: values.MFA_PHONE_OTP_LENGTH,
  249. MFA_PHONE_TEMPLATE: values.MFA_PHONE_TEMPLATE,
  250. MFA_PHONE_ENROLL_ENABLED,
  251. MFA_PHONE_VERIFY_ENABLED,
  252. }
  253. }
  254. setIsUpdatingPhoneForm(true)
  255. updateAuthConfig(
  256. { projectRef: projectRef!, config: payload },
  257. {
  258. onError: (error) => {
  259. toast.error(`Failed to update phone MFA settings: ${error?.message}`)
  260. setIsUpdatingPhoneForm(false)
  261. },
  262. onSuccess: () => {
  263. toast.success('Successfully updated phone MFA settings')
  264. setIsUpdatingPhoneForm(false)
  265. },
  266. }
  267. )
  268. }
  269. if (isError) {
  270. return (
  271. <PageSection>
  272. <PageSectionContent>
  273. <AlertError error={authConfigError} subject="Failed to retrieve auth configuration" />
  274. </PageSectionContent>
  275. </PageSection>
  276. )
  277. }
  278. if (!canReadConfig) {
  279. return (
  280. <PageSection>
  281. <PageSectionContent>
  282. <NoPermission resourceText="view auth configuration settings" />
  283. </PageSectionContent>
  284. </PageSection>
  285. )
  286. }
  287. if (isLoading || isLoadingEntitlement || isLoadingEntitlementEnhanceSecurity) {
  288. return (
  289. <PageSection>
  290. <PageSectionContent>
  291. <GenericSkeletonLoader />
  292. </PageSectionContent>
  293. </PageSection>
  294. )
  295. }
  296. const phoneMFAIsEnabled =
  297. phoneForm.watch('MFA_PHONE') === 'Enabled' || phoneForm.watch('MFA_PHONE') === 'Verify Enabled'
  298. const hasUpgradedPhoneMFA =
  299. authConfig && !authConfig.MFA_PHONE_VERIFY_ENABLED && phoneMFAIsEnabled
  300. const maybeConfirmPhoneMFAOrSubmit = () => {
  301. if (hasUpgradedPhoneMFA) {
  302. setIsConfirmationModalVisible(true)
  303. } else {
  304. phoneForm.handleSubmit(onSubmitPhoneForm)()
  305. }
  306. }
  307. return (
  308. <>
  309. <PageSection>
  310. <PageSectionMeta>
  311. <PageSectionSummary>
  312. <PageSectionTitle>Multi-Factor Authentication (MFA)</PageSectionTitle>
  313. </PageSectionSummary>
  314. </PageSectionMeta>
  315. <PageSectionContent>
  316. <Form {...totpForm}>
  317. <form onSubmit={totpForm.handleSubmit(onSubmitTotpForm)} className="space-y-4">
  318. <Card>
  319. <CardContent>
  320. <FormField
  321. control={totpForm.control}
  322. name="MFA_TOTP"
  323. render={({ field }) => (
  324. <FormItemLayout
  325. layout="flex-row-reverse"
  326. label="TOTP (App Authenticator)"
  327. description="Control use of TOTP (App Authenticator) factors"
  328. >
  329. <FormControl>
  330. <Select
  331. value={field.value}
  332. onValueChange={field.onChange}
  333. disabled={!canUpdateConfig}
  334. >
  335. <SelectTrigger>
  336. <SelectValue placeholder="Select status" />
  337. </SelectTrigger>
  338. <SelectContent>
  339. {MFAFactorSelectionOptions.map((option) => (
  340. <SelectItem key={option.value} value={option.value}>
  341. {option.label}
  342. </SelectItem>
  343. ))}
  344. </SelectContent>
  345. </Select>
  346. </FormControl>
  347. </FormItemLayout>
  348. )}
  349. />
  350. </CardContent>
  351. <CardContent>
  352. <FormField
  353. control={totpForm.control}
  354. name="MFA_MAX_ENROLLED_FACTORS"
  355. render={({ field }) => (
  356. <FormItemLayout
  357. layout="flex-row-reverse"
  358. label="Maximum number of per-user MFA factors"
  359. description="How many MFA factors can be enrolled at once per user."
  360. >
  361. <FormControl>
  362. <InputGroup>
  363. <FormInputGroupInput
  364. type="number"
  365. min={0}
  366. max={30}
  367. {...field}
  368. disabled={!canUpdateConfig}
  369. data-1p-ignore // 1Password
  370. data-lpignore="true" // LastPass
  371. data-form-type="other" // Dashlane
  372. data-bwignore // Bitwarden
  373. />
  374. <InputGroupAddon align="inline-end">
  375. <InputGroupText>factors</InputGroupText>
  376. </InputGroupAddon>
  377. </InputGroup>
  378. </FormControl>
  379. </FormItemLayout>
  380. )}
  381. />
  382. </CardContent>
  383. <CardFooter className="justify-end space-x-2">
  384. {totpForm.formState.isDirty && (
  385. <Button type="default" onClick={() => totpForm.reset()}>
  386. Cancel
  387. </Button>
  388. )}
  389. <Button
  390. type="primary"
  391. htmlType="submit"
  392. disabled={!canUpdateConfig || isUpdatingTotpForm || !totpForm.formState.isDirty}
  393. loading={isUpdatingTotpForm}
  394. >
  395. Save changes
  396. </Button>
  397. </CardFooter>
  398. </Card>
  399. </form>
  400. </Form>
  401. </PageSectionContent>
  402. </PageSection>
  403. <PageSection>
  404. <PageSectionMeta>
  405. <PageSectionSummary>
  406. <PageSectionTitle>SMS MFA</PageSectionTitle>
  407. </PageSectionSummary>
  408. </PageSectionMeta>
  409. <PageSectionContent>
  410. <Form {...phoneForm}>
  411. <form
  412. onSubmit={(e) => {
  413. e.preventDefault()
  414. maybeConfirmPhoneMFAOrSubmit()
  415. }}
  416. >
  417. <Card>
  418. <CardContent>
  419. <FormField
  420. control={phoneForm.control}
  421. name="MFA_PHONE"
  422. render={({ field }) => (
  423. <FormItemLayout
  424. layout="flex-row-reverse"
  425. label="Phone"
  426. description="Control use of phone factors"
  427. >
  428. <FormControl>
  429. <Select
  430. value={field.value}
  431. onValueChange={field.onChange}
  432. disabled={!canUpdateConfig || !hasAccessToMFA}
  433. >
  434. <SelectTrigger>
  435. <SelectValue placeholder="Select status" />
  436. </SelectTrigger>
  437. <SelectContent>
  438. {MFAFactorSelectionOptions.map((option) => (
  439. <SelectItem key={option.value} value={option.value}>
  440. {option.label}
  441. </SelectItem>
  442. ))}
  443. </SelectContent>
  444. </Select>
  445. </FormControl>
  446. </FormItemLayout>
  447. )}
  448. />
  449. {!hasValidMFAProvider && phoneMFAIsEnabled && (
  450. <Alert variant="warning" className="mt-3">
  451. <WarningIcon />
  452. <AlertTitle>
  453. To use MFA with Phone you should set up a Phone provider or Send SMS Hook.
  454. </AlertTitle>
  455. </Alert>
  456. )}
  457. </CardContent>
  458. <CardContent>
  459. <FormField
  460. control={phoneForm.control}
  461. name="MFA_PHONE_OTP_LENGTH"
  462. render={({ field }) => (
  463. <FormItemLayout
  464. layout="flex-row-reverse"
  465. label="Phone OTP Length"
  466. description="Number of digits in OTP"
  467. >
  468. <FormControl>
  469. <InputGroup>
  470. <FormInputGroupInput
  471. type="number"
  472. min={6}
  473. max={30}
  474. {...field}
  475. disabled={!canUpdateConfig || !hasAccessToMFA}
  476. data-1p-ignore // 1Password
  477. data-lpignore="true" // LastPass
  478. data-form-type="other" // Dashlane
  479. data-bwignore // Bitwarden
  480. />
  481. <InputGroupAddon align="inline-end">
  482. <InputGroupText>digits</InputGroupText>
  483. </InputGroupAddon>
  484. </InputGroup>
  485. </FormControl>
  486. </FormItemLayout>
  487. )}
  488. />
  489. </CardContent>
  490. <CardContent>
  491. <FormField
  492. control={phoneForm.control}
  493. name="MFA_PHONE_TEMPLATE"
  494. render={({ field }) => (
  495. <FormItemLayout
  496. layout="flex-row-reverse"
  497. label="Phone verification message"
  498. description="To format the OTP code use `{{ .Code }}`"
  499. >
  500. <FormControl>
  501. <Input
  502. type="text"
  503. {...field}
  504. disabled={!canUpdateConfig || !hasAccessToMFA}
  505. data-1p-ignore // 1Password
  506. data-lpignore="true" // LastPass
  507. data-form-type="other" // Dashlane
  508. data-bwignore // Bitwarden
  509. />
  510. </FormControl>
  511. </FormItemLayout>
  512. )}
  513. />
  514. </CardContent>
  515. {promptProPlanUpgrade && (
  516. <UpgradeToPro
  517. fullWidth
  518. source="authSmsMfa"
  519. featureProposition="configure settings for SMS MFA"
  520. primaryText="SMS MFA is only available on the Pro Plan and above"
  521. secondaryText="Upgrade to the Pro plan to configure settings for SMS MFA."
  522. />
  523. )}
  524. <CardFooter className="justify-end space-x-2">
  525. {phoneForm.formState.isDirty && (
  526. <Button type="default" onClick={() => phoneForm.reset()}>
  527. Cancel
  528. </Button>
  529. )}
  530. <Button
  531. type={promptProPlanUpgrade ? 'default' : 'primary'}
  532. htmlType="submit"
  533. disabled={
  534. !canUpdateConfig ||
  535. isUpdatingPhoneForm ||
  536. !phoneForm.formState.isDirty ||
  537. !hasAccessToMFA
  538. }
  539. loading={isUpdatingPhoneForm}
  540. >
  541. Save changes
  542. </Button>
  543. </CardFooter>
  544. </Card>
  545. </form>
  546. </Form>
  547. </PageSectionContent>
  548. </PageSection>
  549. <ConfirmationModal
  550. visible={isConfirmationModalVisible}
  551. title="Confirm SMS MFA"
  552. confirmLabel="Confirm and save"
  553. onCancel={() => setIsConfirmationModalVisible(false)}
  554. onConfirm={() => {
  555. setIsConfirmationModalVisible(false)
  556. phoneForm.handleSubmit(onSubmitPhoneForm)()
  557. }}
  558. variant="warning"
  559. >
  560. Enabling SMS MFA will result in an additional charge of <span translate="no">$75</span> per
  561. month for the first project in the organization and an additional{' '}
  562. <span translate="no">$10</span> per month for additional projects.
  563. <p className="mt-2">
  564. Billing will start immediately upon enabling this add-on, regardless of whether your
  565. customers are using SMS MFA.
  566. </p>
  567. <TaxDisclaimer className="mt-2" />
  568. </ConfirmationModal>
  569. <PageSection>
  570. <PageSectionMeta>
  571. <PageSectionSummary>
  572. <PageSectionTitle>Enhanced MFA Security</PageSectionTitle>
  573. </PageSectionSummary>
  574. </PageSectionMeta>
  575. <PageSectionContent>
  576. <Form {...securityForm}>
  577. <form onSubmit={securityForm.handleSubmit(onSubmitSecurityForm)}>
  578. <Card>
  579. <CardContent>
  580. <FormField
  581. control={securityForm.control}
  582. name="MFA_ALLOW_LOW_AAL"
  583. render={({ field }) => (
  584. <FormItemLayout
  585. layout="flex-row-reverse"
  586. label="Limit duration of AAL1 sessions"
  587. description="A user's session will be terminated unless they verify one of their factors within 15 minutes of initial sign in. Recommendation: ON"
  588. >
  589. <FormControl>
  590. <Switch
  591. checked={!field.value}
  592. onCheckedChange={(value) => field.onChange(!value)}
  593. disabled={!canUpdateConfig || !hasAccessToEnhanceSecurity}
  594. />
  595. </FormControl>
  596. </FormItemLayout>
  597. )}
  598. />
  599. </CardContent>
  600. {promptEnhancedSecurityUpgrade && (
  601. <UpgradeToPro
  602. fullWidth
  603. source="authEnhancedSecurity"
  604. featureProposition="configure settings for Enhanced MFA Security"
  605. primaryText="Enhanced MFA Security is not available on your plan"
  606. secondaryText="Upgrade your plan to configure settings for Enhanced MFA Security"
  607. buttonText="Upgrade"
  608. />
  609. )}
  610. <CardFooter className="justify-end space-x-2">
  611. {securityForm.formState.isDirty && (
  612. <Button type="default" onClick={() => securityForm.reset()}>
  613. Cancel
  614. </Button>
  615. )}
  616. <Button
  617. type="primary"
  618. htmlType="submit"
  619. disabled={
  620. !canUpdateConfig || isUpdatingSecurityForm || !securityForm.formState.isDirty
  621. }
  622. loading={isUpdatingSecurityForm}
  623. >
  624. Save changes
  625. </Button>
  626. </CardFooter>
  627. </Card>
  628. </form>
  629. </Form>
  630. </PageSectionContent>
  631. </PageSection>
  632. </>
  633. )
  634. }