From e1a4566cc3a1ce8e7cd91eefedeaf0e31468546a Mon Sep 17 00:00:00 2001 From: cclohmar Date: Wed, 15 Jul 2026 14:11:39 +0100 Subject: [PATCH] feat: enforce TOTP enrollment via SQLite when email is saved --- compose/stack.yaml | 2 +- main.go | 126 ++++++++++++++++++++++----------------------- 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/compose/stack.yaml b/compose/stack.yaml index 09419fc..5894f81 100644 --- a/compose/stack.yaml +++ b/compose/stack.yaml @@ -54,7 +54,7 @@ services: command: - sh - -c - - "apk add --no-cache curl >/dev/null 2>&1 && exec /opt/nextworkspace/nextworkspace" + - "apk add --no-cache curl sqlite3 >/dev/null 2>&1 && exec /opt/nextworkspace/nextworkspace" environment: - CONFIG_DIR=/opt/nextworkspace/config/nextworkspace - AUTHELIA_SECRET={AUTHELIA_SECRET} diff --git a/main.go b/main.go index 4115ff8..f03872a 100644 --- a/main.go +++ b/main.go @@ -864,37 +864,53 @@ func csvImportHandler(w http.ResponseWriter, r *http.Request) { }) } -// --- MFA status check --- +// --- MFA enforcement --- -func checkMFAStatus(w http.ResponseWriter, r *http.Request) { +// Check if user has TOTP enrolled by querying Authelia's SQLite database directly. +func checkTOTPEnrolled(username string) bool { + dbPath := "/opt/nextworkspace/data/authelia/db.sqlite" + out, err := exec.Command("sqlite3", dbPath, + "SELECT COUNT(*) FROM totp_configurations WHERE username='"+username+"'").Output() + if err != nil { + return false + } + return strings.TrimSpace(string(out)) == "1" +} + +// Set user's preferred 2FA method to totp, triggering enrollment prompt on next login. +func enforceTOTP(w http.ResponseWriter, r *http.Request) { user := r.Header.Get("Remote-User") if user == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - // Attempt to call Authelia API to check TOTP status. - // This requires an active user session (firstfactor completed), - // so it may return 403 if called server-side without session. - token := os.Getenv("AUTHELIA_SECRET") - req, _ := http.NewRequest("GET", "http://authelia:9091/api/user/info", nil) - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := http.DefaultClient.Do(req) - if err != nil || resp.StatusCode != 200 { - // Can't verify TOTP status server-side. Show prompt if email is set. - json.NewEncoder(w).Encode(map[string]bool{"mfa_enabled": false}) + if checkTOTPEnrolled(user) { + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "already_enrolled", + "totp_required": false, + }) return } - defer resp.Body.Close() - var userInfo struct { - TOTP bool `json:"totp"` + // Update Authelia's user_preferences to require TOTP on next login + dbPath := "/opt/nextworkspace/data/authelia/db.sqlite" + cmd := exec.Command("sqlite3", dbPath, + "INSERT OR REPLACE INTO user_preferences (id, username, method) VALUES ((SELECT id FROM user_preferences WHERE username='"+user+"'), '"+user+"', 'totp')") + err := cmd.Run() + + if err != nil { + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "error", + "error": "Failed to update preferences", + "totp_required": true, + }) + return } - json.NewDecoder(resp.Body).Decode(&userInfo) - json.NewEncoder(w).Encode(map[string]bool{ - "mfa_enabled": userInfo.TOTP, + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "enforced", + "totp_required": true, }) } @@ -1168,26 +1184,7 @@ const settingsHTML = ` {{t .Lang "saved"}} - - + {{if .IsAdmin}}
@@ -1198,39 +1195,40 @@ const settingsHTML = ` {{end}}