From 0f802de51d4db275769e4dc02b7ff03e5da66941 Mon Sep 17 00:00:00 2001 From: Leopere Date: Wed, 4 Mar 2026 18:13:11 -0500 Subject: [PATCH] Fix password reset trigger: add debug logging, response body parsing, displayName in LDAP - triggerPasswordReset now logs the full URL, status, and response body - Detects Authelia "KO" status responses as errors - Forwards real client IP instead of 127.0.0.1 - Sets displayName=email on LDAP user creation for friendly email greetings - Backfills displayName for existing users on re-provision Made-with: Cursor --- docker/ss-atlas/internal/handlers/authelia.go | 47 +++++++++++++++---- .../internal/handlers/subscription.go | 2 +- docker/ss-atlas/internal/ldap/client.go | 14 ++++++ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/docker/ss-atlas/internal/handlers/authelia.go b/docker/ss-atlas/internal/handlers/authelia.go index 2272ae3..142c768 100644 --- a/docker/ss-atlas/internal/handlers/authelia.go +++ b/docker/ss-atlas/internal/handlers/authelia.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/json" "fmt" + "io" "log" + "net" "net/http" "strconv" "strings" @@ -29,7 +31,7 @@ func (a *App) handleResendReset(w http.ResponseWriter, r *http.Request) { return } - if err := a.triggerPasswordReset(username); err != nil { + if err := a.triggerPasswordReset(r, username); err != nil { log.Printf("resend-reset: failed for %s: %v", username, err) respondResendError(w, http.StatusInternalServerError, "failed to send email", 0) return @@ -53,19 +55,33 @@ func respondResendError(w http.ResponseWriter, code int, msg string, retryAfter json.NewEncoder(w).Encode(body) } -func (a *App) triggerPasswordReset(username string) error { +func clientIP(r *http.Request) string { + if s := r.Header.Get("X-Forwarded-For"); s != "" { + if idx := strings.Index(s, ","); idx > 0 { + return strings.TrimSpace(s[:idx]) + } + return strings.TrimSpace(s) + } + if s := r.Header.Get("X-Real-IP"); s != "" { + return s + } + if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + return host + } + return r.RemoteAddr +} + +func (a *App) triggerPasswordReset(r *http.Request, username string) error { + url := a.cfg.AutheliaInternalURL + "/api/reset-password/identity/start" body, _ := json.Marshal(map[string]string{"username": username}) - req, err := http.NewRequest( - http.MethodPost, - a.cfg.AutheliaInternalURL+"/api/reset-password/identity/start", - bytes.NewReader(body), - ) + log.Printf("triggerPasswordReset: POST %s for user %q", url, username) + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { return fmt.Errorf("authelia reset build request: %w", err) } - // Strip scheme from AutheliaURL to get the host for forwarding headers externalHost := strings.TrimPrefix(strings.TrimPrefix(a.cfg.AutheliaURL, "https://"), "http://") proto := "http" if strings.HasPrefix(a.cfg.AutheliaURL, "https://") { @@ -75,7 +91,7 @@ func (a *App) triggerPasswordReset(username string) error { req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Forwarded-Host", externalHost) req.Header.Set("X-Forwarded-Proto", proto) - req.Header.Set("X-Forwarded-For", "127.0.0.1") + req.Header.Set("X-Forwarded-For", clientIP(r)) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -83,8 +99,19 @@ func (a *App) triggerPasswordReset(username string) error { } defer resp.Body.Close() + respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + log.Printf("triggerPasswordReset: status=%d body=%s", resp.StatusCode, string(respBody)) + if resp.StatusCode != http.StatusOK { - return fmt.Errorf("authelia reset returned %d", resp.StatusCode) + return fmt.Errorf("authelia reset returned %d: %s", resp.StatusCode, string(respBody)) } + + var result struct { + Status string `json:"status"` + } + if json.Unmarshal(respBody, &result) == nil && result.Status == "KO" { + return fmt.Errorf("authelia reset rejected: %s", string(respBody)) + } + return nil } diff --git a/docker/ss-atlas/internal/handlers/subscription.go b/docker/ss-atlas/internal/handlers/subscription.go index 918a97d..9eb2788 100644 --- a/docker/ss-atlas/internal/handlers/subscription.go +++ b/docker/ss-atlas/internal/handlers/subscription.go @@ -153,7 +153,7 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) { if result.IsNew || !inGroup { // New or lapsed: send password email, show success page. - if err := a.triggerPasswordReset(result.Username); err != nil { + if err := a.triggerPasswordReset(r, result.Username); err != nil { log.Printf("authelia reset trigger failed for %s: %v", username, err) } else { resendRateLimiter.record(result.Username) diff --git a/docker/ss-atlas/internal/ldap/client.go b/docker/ss-atlas/internal/ldap/client.go index 7869fc7..add9360 100644 --- a/docker/ss-atlas/internal/ldap/client.go +++ b/docker/ss-atlas/internal/ldap/client.go @@ -58,6 +58,7 @@ func (c *Client) ProvisionUser(username, email, stripeCustomerID, phone string) if phone != "" { _ = c.SetCustomerPhone(username, phone) } + _ = c.ensureDisplayName(conn, username, email) return &ProvisionResult{Username: username, IsNew: false}, nil } @@ -70,6 +71,7 @@ func (c *Client) ProvisionUser(username, email, stripeCustomerID, phone string) addReq.Attribute("sn", []string{username}) addReq.Attribute("uid", []string{username}) addReq.Attribute("mail", []string{email}) + addReq.Attribute("displayName", []string{email}) if phone != "" { addReq.Attribute("telephoneNumber", []string{phone}) } @@ -98,6 +100,18 @@ func (c *Client) ProvisionUser(username, email, stripeCustomerID, phone string) return &ProvisionResult{Username: username, Password: password, IsNew: true}, nil } +func (c *Client) ensureDisplayName(conn *goldap.Conn, username, email string) error { + userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN) + modReq := goldap.NewModifyRequest(userDN, nil) + modReq.Replace("displayName", []string{email}) + if err := conn.Modify(modReq); err != nil { + log.Printf("ldap ensure displayName for %s: %v (may already be set)", username, err) + return err + } + log.Printf("ldap set displayName for %s to %s", username, email) + return nil +} + func (c *Client) EnsureUser(username, email, stripeCustomerID, phone string) error { _, err := c.ProvisionUser(username, email, stripeCustomerID, phone) return err