diff --git a/backend/config.py b/backend/config.py
index 04039e5adbf896b51b5a5958672c48ea229776ce..7f9c276e90e8bcd89e235bd6c26e7baf04ef1cb3 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -24,3 +24,4 @@ LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG").lower() == "true
 RUN_BY_GUNICORN = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
 
 DEMO_INSTANCE = os.environ.get("DASHBOARD_DEMO_INSTANCE", "False").lower() in ('true', '1')
+ENFORCE_2FA = os.environ.get("DASHBOARD_ENFORCE_2FA", "False").lower() in ('true', '1')
diff --git a/backend/web/login/login.py b/backend/web/login/login.py
index 6947038ec10f7c4445e4eda90875750049326f0e..71117746cc5f5e7f27243c78dca1a3cf18d11bf0 100644
--- a/backend/web/login/login.py
+++ b/backend/web/login/login.py
@@ -17,6 +17,7 @@ from ory_hydra_client.models import AcceptConsentRequest, AcceptLoginRequest, Co
 import ory_hydra_client.exceptions as hydra_exceptions
 import ory_kratos_client
 from ory_kratos_client.api import frontend_api, identity_api
+from ory_kratos_client.model.authenticator_assurance_level import AuthenticatorAssuranceLevel
 from flask import abort, current_app, jsonify, redirect, render_template, request
 
 from database import db
@@ -335,6 +336,24 @@ def consent():
         current_app.logger.error(f"Conflict. Consent request {challenge} already used")
         abort(503, description="Consent request already used. Please try again")
 
+    if ENFORCE_2FA:
+        # Check for session status, in particular 2FA.
+        cookie = get_kratos_cookie()
+        if not cookie:
+            current_app.logger.info("consent: no kratos cookie set, redirecting to set up 2fa")
+            response = redirect(KRATOS_PUBLIC_URL + "self-service/settings/browser")
+            response.set_cookie('stackspin_context', '2fa-required')
+            return response
+        session = kratos_public_frontend_api.to_session(cookie=cookie)
+        # Check session aal.
+        aal = session['authenticator_assurance_level']
+        current_app.logger.info(f"aal: {aal}")
+        if aal == AuthenticatorAssuranceLevel('aal1'):
+            current_app.logger.info("aal is only aal1, so not accepting consent request")
+            response = redirect(KRATOS_PUBLIC_URL + "self-service/settings/browser")
+            response.set_cookie('stackspin_context', '2fa-required')
+            return response
+
     # Get information about this consent request:
     # False positive: pylint: disable=no-member
     try:
diff --git a/backend/web/static/base.js b/backend/web/static/base.js
index bf79733c8c7eaf990a84556bf5753d67430be9e9..d362b57907208b006c1ee3ef70b5ef590c76f168 100644
--- a/backend/web/static/base.js
+++ b/backend/web/static/base.js
@@ -218,6 +218,16 @@ function flow_settings() {
 				$('#pills-password-tab').tab('show');
 				Cookies.set('stackspin_context', '');
 			}
+
+                        // If the user is required to set up 2FA, switch to
+                        // that tab and show a message.
+			if (context == "2fa-required") {
+				$('#pills-totp-tab').tab('show');
+                                $("#contentMessages").html('Setting up a second factor is required to continue.');
+                                $("#contentMessages").addClass("alert");
+                                $("#contentMessages").addClass("alert-warning");
+				Cookies.set('stackspin_context', '');
+			}
 		},
 		complete: function (obj) {
 			// If we get a 410, the flow is expired, need to refresh the flow