forked from Nixius/authelia
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func (a *App) handleResendReset(w http.ResponseWriter, r *http.Request) {
|
|
username := r.FormValue("username")
|
|
if username == "" {
|
|
if email := r.FormValue("email"); email != "" {
|
|
username = sanitizeUsername(email)
|
|
}
|
|
}
|
|
if username == "" {
|
|
respondResendError(w, http.StatusBadRequest, "email or username required", 0)
|
|
return
|
|
}
|
|
|
|
if ok, retryAfter := resendRateLimiter.allow(username); !ok {
|
|
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
|
|
respondResendError(w, http.StatusTooManyRequests,
|
|
"please wait before requesting another email", retryAfter)
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
resendRateLimiter.record(username)
|
|
log.Printf("resend-reset: password reset email sent for %s", username)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"ok": true,
|
|
"message": "Password setup email sent. Check your inbox.",
|
|
})
|
|
}
|
|
|
|
func respondResendError(w http.ResponseWriter, code int, msg string, retryAfter int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
body := map[string]any{"ok": false, "error": msg}
|
|
if retryAfter > 0 {
|
|
body["retry_after_seconds"] = retryAfter
|
|
}
|
|
json.NewEncoder(w).Encode(body)
|
|
}
|
|
|
|
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})
|
|
|
|
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)
|
|
}
|
|
|
|
externalHost := strings.TrimPrefix(strings.TrimPrefix(a.cfg.AutheliaURL, "https://"), "http://")
|
|
proto := "http"
|
|
if strings.HasPrefix(a.cfg.AutheliaURL, "https://") {
|
|
proto = "https"
|
|
}
|
|
|
|
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", clientIP(r))
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("authelia reset request: %w", err)
|
|
}
|
|
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: %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
|
|
}
|