diff --git a/app/views/authentication_providers/_canvas_fields.html.erb b/app/views/authentication_providers/_canvas_fields.html.erb
index a148375c6dc..f42aa85ca17 100644
--- a/app/views/authentication_providers/_canvas_fields.html.erb
+++ b/app/views/authentication_providers/_canvas_fields.html.erb
@@ -44,4 +44,8 @@
<% end %>
+ <% if @domain_root_account.feature_enabled?(:password_complexity)%>
+ <% js_bundle :password_complexity_configuration %>
+
+ <% end %>
diff --git a/ui/featureBundles.ts b/ui/featureBundles.ts
index e5cca5ec6c2..3bc44d186bf 100644
--- a/ui/featureBundles.ts
+++ b/ui/featureBundles.ts
@@ -156,6 +156,8 @@ const featureBundles: {
outcome_alignments: () => import('./features/outcome_alignments/index'),
outcome_management: () => import('./features/outcome_management/index'),
page_views: () => import('./features/page_views/index'),
+ password_complexity_configuration: () =>
+ import('./features/password_complexity_configuration/index'),
past_global_alert: () => import('./features/past_global_alert/index'),
past_global_announcements: () => import('./features/past_global_announcements/index'),
permissions: () => import('./features/permissions/index'),
diff --git a/ui/features/password_complexity_configuration/index.tsx b/ui/features/password_complexity_configuration/index.tsx
new file mode 100644
index 00000000000..de369ef6989
--- /dev/null
+++ b/ui/features/password_complexity_configuration/index.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+import ready from '@instructure/ready'
+import React from 'react'
+import ReactDOM from 'react-dom'
+import PasswordComplexityConfiguration from './react/PasswordComplexityConfiguration'
+
+ready(() => {
+ const container = document.querySelector('#password_complexity_configuration')
+
+ if (container) {
+ ReactDOM.render(, container)
+ }
+})
diff --git a/ui/features/password_complexity_configuration/package.json b/ui/features/password_complexity_configuration/package.json
new file mode 100644
index 00000000000..1f5bf39318a
--- /dev/null
+++ b/ui/features/password_complexity_configuration/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@canvas-features/password_complexity_configuration",
+ "private": true,
+ "version": "1.0.0",
+ "owner": "FOO"
+}
\ No newline at end of file
diff --git a/ui/features/password_complexity_configuration/react/NumberInputControlled.tsx b/ui/features/password_complexity_configuration/react/NumberInputControlled.tsx
new file mode 100644
index 00000000000..1764c8bde67
--- /dev/null
+++ b/ui/features/password_complexity_configuration/react/NumberInputControlled.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+import React, {useState} from 'react'
+import {NumberInput} from '@instructure/ui-number-input'
+
+interface NumberInputControlledProps {
+ minimum: number
+ maximum: number
+ defaultValue: number
+ disabled: boolean
+ 'data-testid': string
+}
+
+const NumberInputControlled: React.FC = ({
+ minimum,
+ maximum,
+ defaultValue,
+ disabled,
+ 'data-testid': dataTestid,
+}) => {
+ const [number, setNumber] = useState(defaultValue)
+ const handleChange = (event: React.ChangeEvent, value: string) => {
+ setNumber(value ? Number(value) : 0) // Replace null with 0
+ }
+
+ const handleDecrement = () => {
+ if (Number.isNaN(number)) return
+ if (number === null) setBoundedNumber(minimum)
+ else setBoundedNumber(Math.floor(number) - 1)
+ }
+
+ const handleIncrement = () => {
+ if (Number.isNaN(number)) return
+ if (number === null) setBoundedNumber(minimum + 1)
+ else setBoundedNumber(Math.ceil(number) + 1)
+ }
+
+ const handleBlur = () => {
+ if (Number.isNaN(number)) return
+ if (number === null) return
+ setBoundedNumber(Math.round(number))
+ }
+
+ const setBoundedNumber = (n: number) => {
+ if (n < minimum) setNumber(minimum)
+ else if (n > maximum) setNumber(maximum)
+ else setNumber(n)
+ }
+
+ return (
+
+ )
+}
+
+export default NumberInputControlled
diff --git a/ui/features/password_complexity_configuration/react/PasswordComplexityConfiguration.tsx b/ui/features/password_complexity_configuration/react/PasswordComplexityConfiguration.tsx
new file mode 100644
index 00000000000..212d14eec74
--- /dev/null
+++ b/ui/features/password_complexity_configuration/react/PasswordComplexityConfiguration.tsx
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2024 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+import React, {useState} from 'react'
+import {useScope as useI18nScope} from '@canvas/i18n'
+import {Heading} from '@instructure/ui-heading'
+import {Text} from '@instructure/ui-text'
+import {View} from '@instructure/ui-view'
+import {Button, CloseButton, IconButton} from '@instructure/ui-buttons'
+import {Tray} from '@instructure/ui-tray'
+import {Alert} from '@instructure/ui-alerts'
+import {List} from '@instructure/ui-list'
+import {Checkbox} from '@instructure/ui-checkbox'
+import NumberInputControlled from './NumberInputControlled'
+import {Link} from '@instructure/ui-link'
+import {IconTrashLine, IconUploadSolid} from '@instructure/ui-icons'
+import {Flex} from '@instructure/ui-flex'
+import {Modal} from '@instructure/ui-modal'
+import {FileDrop} from '@instructure/ui-file-drop'
+import {Billboard} from '@instructure/ui-billboard'
+import {Img} from '@instructure/ui-img'
+
+const I18n = useI18nScope('password_complexity_configuration')
+
+const PasswordComplexityConfiguration = () => {
+ const [showTray, setShowTray] = useState(false)
+ const [minimumCharacterLengthEnabled, setMinimumCharacterLengthEnabled] = useState(true)
+ const [requireNumbersEnabled, setRequireNumbersEnabled] = useState(true)
+ const [requireSymbolsEnabled, setRequireSymbolsEnabled] = useState(true)
+ const [customForbiddenWordsEnabled, setCustomForbiddenWordsEnabled] = useState(false)
+ const [customListFile, setCustomListFile] = useState(null)
+ const [customListUploaded, setCustomListUploaded] = useState(false)
+ const [fileModalOpen, setFileModalOpen] = useState(false)
+ const [customMaxLoginAttemptsEnabled, setCustomMaxLoginAttemptsEnabled] = useState(false)
+
+ const handleCustomMaxLoginAttemptToggle = (event: React.ChangeEvent) => {
+ const checked = event.target.checked
+ setCustomMaxLoginAttemptsEnabled(checked)
+ }
+
+ const handleFileDrop = (file: File) => {
+ setCustomListFile(file)
+ }
+
+ const handleCancelModal = () => {
+ setFileModalOpen(false)
+ setCustomListFile(null)
+ }
+
+ const handleUploadModal = () => {
+ setCustomListUploaded(true)
+ setFileModalOpen(false)
+ }
+
+ const handleDeleteCustomList = () => {
+ setCustomListFile(null)
+ setCustomListUploaded(false)
+ }
+
+ return (
+ <>
+
+ {I18n.t('Password Options')}
+
+
+ setShowTray(false)}
+ placement="end"
+ size="medium"
+ >
+
+
+
+
+
+ {I18n.t('Password Options')}
+
+
+
+ setShowTray(false)}
+ />
+
+
+
+
+
+ {I18n.t(
+ 'Some institutions have very strict policies regarding passwords. This feature enables customization of password requirements and options for this auth provider. Modifications to password options will customize the password configuration text as seen below.'
+ )}
+
+
+ {I18n.t('Current Password Configuration')}
+
+
+ {I18n.t('Your password must meet the following requirements')}
+
+
+ {I18n.t('Must be at least 8 Characters in length.')}
+
+ {I18n.t(
+ 'Must not use words or sequences of characters common in passwords (ie: password, 12345, etc...)'
+ )}
+
+
+
+
+ setMinimumCharacterLengthEnabled(!minimumCharacterLengthEnabled)}
+ defaultChecked={true}
+ data-testid="minimumCharacterLengthCheckbox"
+ />
+
+
+
+
+
+
+
+ setRequireNumbersEnabled(!requireNumbersEnabled)}
+ defaultChecked={true}
+ data-testid="requireNumbersCheckbox"
+ />
+
+
+ setRequireSymbolsEnabled(!requireSymbolsEnabled)}
+ defaultChecked={true}
+ data-testid="requireSymbolsCheckbox"
+ />
+
+
+
+ {
+ setCustomForbiddenWordsEnabled(!customForbiddenWordsEnabled)
+ }}
+ label={I18n.t('Customize forbidden words/termslist (see default list here)')}
+ data-testid="customForbiddenWordsCheckbox"
+ />
+
+
+
+ {I18n.t(
+ 'Upload a list of forbidden words/terms in addition to the default list. The file should be text file (.txt) with a single word or term per line.'
+ )}
+
+
+ {!customListUploaded && (
+
+
+
+ )}
+
+
+ {customListUploaded && (
+
+ {I18n.t('Current Custom List')}
+
+
+
+
+ {customListFile?.name}
+
+
+ {
+ handleDeleteCustomList()
+ }}
+ >
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ {I18n.t(
+ 'This option controls the number of attempts a single user can make consecutively to login without success before their user’s login is suspended. Users can be unsuspended by institutional admins. Cannot be higher than 20 attempts.'
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setFileModalOpen(false)
+ }}
+ size="medium"
+ label="Upload Forbidden Words/Terms List"
+ shouldCloseOnDocumentClick={true}
+ overflow="scroll"
+ >
+
+ {I18n.t('Upload Forbidden Words/Terms List')}
+ setFileModalOpen(false)}
+ screenReaderLabel="Close"
+ />
+
+
+ {!customListFile && (
+