diff --git a/tests/e2e/test_flows_login_sfe.py b/tests/e2e/test_flows_login_sfe.py index 601aa113a8..c28f2e9953 100644 --- a/tests/e2e/test_flows_login_sfe.py +++ b/tests/e2e/test_flows_login_sfe.py @@ -8,6 +8,8 @@ from selenium.webdriver.remote.webdriver import WebDriver from authentik.blueprints.tests import apply_blueprint from authentik.core.models import User +from authentik.flows.models import NotConfiguredAction +from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses from tests.e2e.utils import SeleniumTestCase, retry @@ -50,3 +52,28 @@ class TestFlowsLoginSFE(SeleniumTestCase): login_sfe(self.driver, self.user) self.wait_for_url(self.if_user_url("/library")) self.assert_user(self.user) + + @retry() + @apply_blueprint( + "default/flow-default-authentication-flow.yaml", + "default/flow-default-invalidation-flow.yaml", + ) + def test_login_mfa_static_deny(self): + """test default login flow""" + mfa = AuthenticatorValidateStage.objects.get( + name="default-authentication-mfa-validation", + ) + mfa.not_configured_action = NotConfiguredAction.DENY + mfa.device_classes = [DeviceClasses.STATIC] + mfa.save() + + self.driver.get( + self.url( + "authentik_core:if-flow", + flow_slug="default-authentication-flow", + query={"sfe": True}, + ) + ) + login_sfe(self.driver, self.user) + msg = self.driver.find_element(By.CSS_SELECTOR, "#access-denied > p") + self.assertEqual(msg.text, "No (allowed) MFA authenticator configured.") diff --git a/web/package-lock.json b/web/package-lock.json index 2cb9a6c6fc..ebcffe6b4a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2636,17 +2636,6 @@ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "29.0.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", @@ -6119,25 +6108,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bootstrap": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", - "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -13707,6 +13677,18 @@ "points-on-curve": "0.2.0" } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -17565,7 +17547,7 @@ "@swc/cli": "^0.7.10", "@swc/core": "^1.15.10", "base64-js": "^1.5.1", - "bootstrap": "^5.3.8", + "bootstrap": "^4.6.2", "formdata-polyfill": "^2025.11.0", "globby": "16.1.0", "jquery": "^3.7.1", @@ -17584,6 +17566,27 @@ "@swc/core-win32-ia32-msvc": "^1.15.3", "@swc/core-win32-x64-msvc": "^1.15.3" } + }, + "packages/sfe/node_modules/bootstrap": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", + "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } } } } diff --git a/web/packages/sfe/package.json b/web/packages/sfe/package.json index 97bade5f65..573a6248a7 100644 --- a/web/packages/sfe/package.json +++ b/web/packages/sfe/package.json @@ -22,7 +22,7 @@ "@swc/cli": "^0.7.10", "@swc/core": "^1.15.10", "base64-js": "^1.5.1", - "bootstrap": "^5.3.8", + "bootstrap": "^4.6.2", "formdata-polyfill": "^2025.11.0", "globby": "16.1.0", "jquery": "^3.7.1", diff --git a/web/packages/sfe/src/index.ts b/web/packages/sfe/src/index.ts index 6877ccf351..053e551206 100644 --- a/web/packages/sfe/src/index.ts +++ b/web/packages/sfe/src/index.ts @@ -2,6 +2,7 @@ import "formdata-polyfill"; import "weakmap-polyfill"; import { + type AccessDeniedChallenge, type AuthenticatorValidationChallenge, type AutosubmitChallenge, type ChallengeTypes, @@ -113,8 +114,16 @@ class SimpleFlowExecutor { case "ak-stage-authenticator-validate": new AuthenticatorValidateStage(this, this.challenge).render(); return; + case "ak-stage-access-denied": + new AccessDeniedStage(this, this.challenge).render(); + return; default: - this.container.innerText = "Unsupported stage: " + this.challenge?.component; + new AccessDeniedStage(this, { + component: "ak-stage-access-denied", + errorMessage: "Unsupported stage: " + this.challenge?.component, + pendingUser: "", + pendingUserAvatar: "", + }).render(); return; } } @@ -492,5 +501,17 @@ class AuthenticatorValidateStage extends Stage } } +class AccessDeniedStage extends Stage { + render() { + this.html(`
+ +

${this.challenge?.flowInfo?.title}

+

+ ${this.challenge.errorMessage ?? "Access denied."} +

+
`); + } +} + const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement); sfe.start();