Auth logout URL, auth proxy

This commit is contained in:
binwiederhier
2026-01-22 20:19:59 -05:00
parent 46cb9f2b41
commit b67ffa4f5f
11 changed files with 104 additions and 35 deletions

View File

@@ -167,6 +167,7 @@ type Config struct {
ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported)
ProxyTrustedPrefixes []netip.Prefix // List of trusted proxy networks (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true
AuthUserHeader string // Header to read the authenticated user from, if BehindProxy is true (e.g. X-Forwarded-User, Remote-User)
AuthLogoutURL string // URL to redirect to when logging out in proxy auth mode (e.g. https://auth.example.com/logout)
StripeSecretKey string
StripeWebhookKey string
StripePriceCacheDuration time.Duration
@@ -265,6 +266,7 @@ func NewConfig() *Config {
BehindProxy: false, // If true, the server will trust the proxy client IP header to determine the client IP address
ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs
AuthUserHeader: "", // Header to read the authenticated user from (requires behind-proxy and auth-file)
AuthLogoutURL: "", // URL to redirect to when logging out in proxy auth mode
StripeSecretKey: "",
StripeWebhookKey: "",
StripePriceCacheDuration: DefaultStripePriceCacheDuration,

View File

@@ -603,13 +603,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor
return s.writeJSON(w, response)
}
func (s *Server) handleConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
func (s *Server) handleConfig(w http.ResponseWriter, _ *http.Request, v *visitor) error {
w.Header().Set("Cache-Control", "no-cache")
return s.writeJSON(w, s.configResponse())
return s.writeJSON(w, s.configResponse(v))
}
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
b, err := json.MarshalIndent(s.configResponse(), "", " ")
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, v *visitor) error {
b, err := json.MarshalIndent(s.configResponse(v), "", " ")
if err != nil {
return err
}
@@ -619,7 +619,15 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
return err
}
func (s *Server) configResponse() *apiConfigResponse {
func (s *Server) configResponse(v *visitor) *apiConfigResponse {
authMode := ""
username := ""
if s.config.AuthUserHeader != "" {
authMode = "proxy"
if v != nil && v.User() != nil {
username = v.User().Name
}
}
return &apiConfigResponse{
BaseURL: "", // Will translate to window.location.origin
AppRoot: s.config.WebRoot,
@@ -635,6 +643,9 @@ func (s *Server) configResponse() *apiConfigResponse {
WebPushPublicKey: s.config.WebPushPublicKey,
DisallowedTopics: s.config.DisallowedTopics,
ConfigHash: s.config.Hash(),
AuthMode: authMode,
AuthLogoutURL: s.config.AuthLogoutURL,
Username: username,
}
}

View File

@@ -139,6 +139,12 @@
#
# auth-user-header:
# If auth-user-header is set, this is the URL to redirect users to when they click logout.
# This is typically the logout URL of your authentication proxy (e.g. Authelia, Authentik).
# If not set, the logout button will be hidden in the web UI when using proxy auth.
#
# auth-logout-url:
# If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments
# are "attachment-cache-dir" and "base-url".
#

View File

@@ -2428,7 +2428,7 @@ func TestServer_AuthUserHeader_UserNotFound(t *testing.T) {
require.Equal(t, errHTTPUnauthorized, err)
}
func TestServer_AuthUserHeader_NoHeader_FallbackToStandardAuth(t *testing.T) {
func TestServer_AuthUserHeader_NoHeader_ReturnsUnauthorized(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = true
c.AuthUserHeader = "X-Forwarded-User"
@@ -2436,28 +2436,27 @@ func TestServer_AuthUserHeader_NoHeader_FallbackToStandardAuth(t *testing.T) {
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
// No X-Forwarded-User header, but with Authorization header
// No X-Forwarded-User header, even with Authorization header -> unauthorized
// When auth-user-header is configured, the header MUST be present
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
r.Header.Set("Authorization", util.BasicAuth("phil", "phil"))
v, err := s.maybeAuthenticate(r)
require.Nil(t, err)
require.NotNil(t, v.User())
require.Equal(t, "phil", v.User().Name)
_, err := s.maybeAuthenticate(r)
require.Equal(t, errHTTPUnauthorized, err)
}
func TestServer_AuthUserHeader_NoHeader_AnonymousAllowed(t *testing.T) {
func TestServer_AuthUserHeader_NoHeader_NoAuthReturnsUnauthorized(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.BehindProxy = true
c.AuthUserHeader = "X-Forwarded-User"
s := newTestServer(t, c)
// No X-Forwarded-User header and no Authorization header -> anonymous
// No X-Forwarded-User header and no Authorization header -> unauthorized
// When auth-user-header is configured, the header MUST be present
r, _ := http.NewRequest("GET", "/mytopic/json?poll=1", nil)
r.RemoteAddr = "1.2.3.4:1234"
v, err := s.maybeAuthenticate(r)
require.Nil(t, err)
require.Nil(t, v.User())
_, err := s.maybeAuthenticate(r)
require.Equal(t, errHTTPUnauthorized, err)
}
func TestServer_AuthUserHeader_NotBehindProxy(t *testing.T) {

View File

@@ -483,6 +483,9 @@ type apiConfigResponse struct {
WebPushPublicKey string `json:"web_push_public_key"`
DisallowedTopics []string `json:"disallowed_topics"`
ConfigHash string `json:"config_hash"`
AuthMode string `json:"auth_mode,omitempty"` // "proxy" if auth-user-header is set, empty otherwise
AuthLogoutURL string `json:"auth_logout_url,omitempty"` // URL to redirect to on logout (only for proxy auth)
Username string `json:"username,omitempty"` // Authenticated username (for proxy auth)
}
type apiAccountBillingPrices struct {