From 40ac403fd248723c422bfb6cde7ac35d094ce709 Mon Sep 17 00:00:00 2001
From: Arie Peterson <arie@greenhost.nl>
Date: Thu, 5 Oct 2023 17:07:32 +0200
Subject: [PATCH] Make it possible to enforce 2FA

---
 backend/config.py          |  1 +
 backend/web/login/login.py | 19 +++++++++++++++++++
 backend/web/static/base.js | 10 ++++++++++
 3 files changed, 30 insertions(+)

diff --git a/backend/config.py b/backend/config.py
index 04039e5a..7f9c276e 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 6947038e..71117746 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 bf79733c..d362b579 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
-- 
GitLab