forked from Nixius/authelia
1
0
Fork 0

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
This commit is contained in:
Leopere 2026-03-04 18:13:11 -05:00
parent e3edf4bb53
commit 0f802de51d
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
3 changed files with 52 additions and 11 deletions

View File

@ -4,7 +4,9 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log" "log"
"net"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -29,7 +31,7 @@ func (a *App) handleResendReset(w http.ResponseWriter, r *http.Request) {
return 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) log.Printf("resend-reset: failed for %s: %v", username, err)
respondResendError(w, http.StatusInternalServerError, "failed to send email", 0) respondResendError(w, http.StatusInternalServerError, "failed to send email", 0)
return return
@ -53,19 +55,33 @@ func respondResendError(w http.ResponseWriter, code int, msg string, retryAfter
json.NewEncoder(w).Encode(body) 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}) body, _ := json.Marshal(map[string]string{"username": username})
req, err := http.NewRequest( log.Printf("triggerPasswordReset: POST %s for user %q", url, username)
http.MethodPost,
a.cfg.AutheliaInternalURL+"/api/reset-password/identity/start", req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
bytes.NewReader(body),
)
if err != nil { if err != nil {
return fmt.Errorf("authelia reset build request: %w", err) 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://") externalHost := strings.TrimPrefix(strings.TrimPrefix(a.cfg.AutheliaURL, "https://"), "http://")
proto := "http" proto := "http"
if strings.HasPrefix(a.cfg.AutheliaURL, "https://") { 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("Content-Type", "application/json")
req.Header.Set("X-Forwarded-Host", externalHost) req.Header.Set("X-Forwarded-Host", externalHost)
req.Header.Set("X-Forwarded-Proto", proto) 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) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
@ -83,8 +99,19 @@ func (a *App) triggerPasswordReset(username string) error {
} }
defer resp.Body.Close() 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 { 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 return nil
} }

View File

@ -153,7 +153,7 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) {
if result.IsNew || !inGroup { if result.IsNew || !inGroup {
// New or lapsed: send password email, show success page. // 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) log.Printf("authelia reset trigger failed for %s: %v", username, err)
} else { } else {
resendRateLimiter.record(result.Username) resendRateLimiter.record(result.Username)

View File

@ -58,6 +58,7 @@ func (c *Client) ProvisionUser(username, email, stripeCustomerID, phone string)
if phone != "" { if phone != "" {
_ = c.SetCustomerPhone(username, phone) _ = c.SetCustomerPhone(username, phone)
} }
_ = c.ensureDisplayName(conn, username, email)
return &ProvisionResult{Username: username, IsNew: false}, nil 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("sn", []string{username})
addReq.Attribute("uid", []string{username}) addReq.Attribute("uid", []string{username})
addReq.Attribute("mail", []string{email}) addReq.Attribute("mail", []string{email})
addReq.Attribute("displayName", []string{email})
if phone != "" { if phone != "" {
addReq.Attribute("telephoneNumber", []string{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 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 { func (c *Client) EnsureUser(username, email, stripeCustomerID, phone string) error {
_, err := c.ProvisionUser(username, email, stripeCustomerID, phone) _, err := c.ProvisionUser(username, email, stripeCustomerID, phone)
return err return err