feat: support HTTP redirect for forward authentication middleware in Traefik (#368)

* feat: support HTTP redirect for forward authentication middleware in Traefik

* fix(docs): fix my terrible merge 

Signed-off-by: Jason Cameron <jasoncameron.all@gmail.com>

* chore: fix typo in docs

Signed-off-by: Jason Cameron <jasoncameron.all@gmail.com>

* fix(ci): add forwardauth

Signed-off-by: Jason Cameron <jasoncameron.all@gmail.com>

* chore: improve doc, target must be a space

* chore: changelog

* fix: validate X-Forwarded headers and check redirect domain

* chore: refactor error handling

* fix(doc): cookie traefik

* fix: tests merge

* Update docs/docs/admin/environments/traefik.mdx

Co-authored-by: Henri Vasserman <henv@hot.ee>
Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Jason Cameron <git@jasoncameron.dev>
Signed-off-by: Jason Cameron <jasoncameron.all@gmail.com>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
Co-authored-by: Jason Cameron <jasoncameron.all@gmail.com>
Co-authored-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Henri Vasserman <henv@hot.ee>
This commit is contained in:
phoval 2025-08-13 02:59:45 +02:00 committed by GitHub
parent 87651f9506
commit a8b7b2ad7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 128 additions and 87 deletions

View file

@ -1,9 +1,11 @@
package lib
import (
"errors"
"fmt"
"math/rand"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
@ -115,8 +117,17 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
localizer := localization.GetLocalizer(r)
if returnHTTPStatusOnly {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(localizer.T("authorization_required")))
if s.opts.PublicUrl == "" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(localizer.T("authorization_required")))
} else {
redirectURL, err := s.constructRedirectURL(r)
if err != nil {
s.respondWithStatus(w, r, err.Error(), http.StatusBadRequest)
return
}
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
}
return
}
@ -196,6 +207,24 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
handler.ServeHTTP(w, r)
}
func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
proto := r.Header.Get("X-Forwarded-Proto")
host := r.Header.Get("X-Forwarded-Host")
uri := r.Header.Get("X-Forwarded-Uri")
if proto == "" || host == "" || uri == "" {
return "", errors.New("missing required X-Forwarded-* headers")
}
// Check if host is allowed in RedirectDomains
if len(s.opts.RedirectDomains) > 0 && !slices.Contains(s.opts.RedirectDomains, host) {
return "", errors.New("redirect domain not allowed")
}
redir := proto + "://" + host + uri
escapedURL := url.QueryEscape(redir)
return fmt.Sprintf("%s/.within.website/?redir=%s", s.opts.PublicUrl, escapedURL), nil
}
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
localizer := localization.GetLocalizer(r)
@ -255,7 +284,12 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
return
}
if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host {
hostNotAllowed := len(urlParsed.Host) > 0 &&
len(s.opts.RedirectDomains) != 0 &&
!slices.Contains(s.opts.RedirectDomains, urlParsed.Host)
hostMismatch := r.URL.Host != "" && urlParsed.Host != r.URL.Host
if hostNotAllowed || hostMismatch {
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), http.StatusBadRequest)
return
}