From 8f046a0b47b1ce0f065fa8a33db98184b07f80d1 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 21 Apr 2025 10:50:37 +1000 Subject: [PATCH 1/9] feat: forward auth This adds basic support for forwarded authentication similar to caddy and traefik. This replaces Authelia SSO as it effectively covers exactly the same use cases. --- src/api.go | 5 +- src/def.go | 12 +- src/mod/auth/sso/authelia/authelia.go | 164 ------------- src/mod/auth/sso/authentik/authentik.go | 169 -------------- src/mod/auth/sso/forward/const.go | 44 ++++ src/mod/auth/sso/forward/forward.go | 294 ++++++++++++++++++++++++ src/mod/dynamicproxy/authProviders.go | 26 +-- src/mod/dynamicproxy/default.go | 13 +- src/mod/dynamicproxy/typedef.go | 22 +- src/mod/update/v315/typedef315.go | 2 +- src/reverseproxy.go | 7 +- src/start.go | 18 +- src/web/components/httprp.html | 13 +- src/web/components/sso.html | 134 ++++------- 14 files changed, 433 insertions(+), 490 deletions(-) delete mode 100644 src/mod/auth/sso/authelia/authelia.go delete mode 100644 src/mod/auth/sso/authentik/authentik.go create mode 100644 src/mod/auth/sso/forward/const.go create mode 100644 src/mod/auth/sso/forward/forward.go diff --git a/src/api.go b/src/api.go index 01adf20..769cf1b 100644 --- a/src/api.go +++ b/src/api.go @@ -80,10 +80,9 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/cert/delete", handleCertRemove) } -// Register the APIs for Authentication handlers like Authelia and OAUTH2 +// Register the APIs for Authentication handlers like Forward Auth and OAUTH2 func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS) - authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS) + authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions) } // Register the APIs for redirection rules management functions diff --git a/src/def.go b/src/def.go index 7efd638..66995ff 100644 --- a/src/def.go +++ b/src/def.go @@ -13,12 +13,10 @@ import ( "net/http" "time" - "imuslab.com/zoraxy/mod/auth/sso/authentik" - "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" - "imuslab.com/zoraxy/mod/auth/sso/authelia" + "imuslab.com/zoraxy/mod/auth/sso/forward" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" @@ -43,8 +41,9 @@ import ( const ( /* Build Constants */ - SYSTEM_NAME = "Zoraxy" - SYSTEM_VERSION = "3.2.1" + SYSTEM_NAME = "Zoraxy" + SYSTEM_VERSION = "3.2.2" + DEVELOPMENT_BUILD = false /* System Constants */ TMP_FOLDER = "./tmp" @@ -144,8 +143,7 @@ var ( pluginManager *plugins.Manager //Plugin manager for managing plugins //Authentication Provider - autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication - authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication + forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication //Helper modules EmailSender *email.Sender //Email sender that handle email sending diff --git a/src/mod/auth/sso/authelia/authelia.go b/src/mod/auth/sso/authelia/authelia.go deleted file mode 100644 index d86f374..0000000 --- a/src/mod/auth/sso/authelia/authelia.go +++ /dev/null @@ -1,164 +0,0 @@ -package authelia - -import ( - "encoding/json" - "errors" - "net" - "net/http" - "net/url" - "strings" - - "imuslab.com/zoraxy/mod/database" - "imuslab.com/zoraxy/mod/info/logger" - "imuslab.com/zoraxy/mod/utils" -) - -type AutheliaRouterOptions struct { - UseHTTPS bool //If the Authelia server is using HTTPS - AutheliaURL string //The URL of the Authelia server - Logger *logger.Logger - Database *database.Database -} - -type AutheliaRouter struct { - options *AutheliaRouterOptions -} - -// NewAutheliaRouter creates a new AutheliaRouter object -func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter { - options.Database.NewTable("authelia") - - //Read settings from database, if exists - options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL) - options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS) - - return &AutheliaRouter{ - options: options, - } -} - -// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS -func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - //Return the current settings - js, _ := json.Marshal(map[string]interface{}{ - "useHTTPS": ar.options.UseHTTPS, - "autheliaURL": ar.options.AutheliaURL, - }) - - utils.SendJSONResponse(w, string(js)) - return - } else if r.Method == http.MethodPost { - //Update the settings - autheliaURL, err := utils.PostPara(r, "autheliaURL") - if err != nil { - utils.SendErrorResponse(w, "autheliaURL not found") - return - } - - useHTTPS, err := utils.PostBool(r, "useHTTPS") - if err != nil { - useHTTPS = false - } - - //Write changes to runtime - ar.options.AutheliaURL = autheliaURL - ar.options.UseHTTPS = useHTTPS - - //Write changes to database - ar.options.Database.Write("authelia", "autheliaURL", autheliaURL) - ar.options.Database.Write("authelia", "useHTTPS", useHTTPS) - - utils.SendOK(w) - } else { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - -} - -// handleAutheliaAuth is the internal handler for Authelia authentication -// Set useHTTPS to true if your authelia server is using HTTPS -// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com -func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { - client := &http.Client{} - - if ar.options.AutheliaURL == "" { - ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil) - w.WriteHeader(500) - w.Write([]byte("500 - Internal Server Error")) - return errors.New("authelia URL not set") - } - protocol := "http" - if ar.options.UseHTTPS { - protocol = "https" - } - - autheliaURL := &url.URL{ - Scheme: protocol, - Host: ar.options.AutheliaURL, - } - - //Make a request to Authelia to verify the request - req, err := http.NewRequest("POST", autheliaURL.JoinPath("api", "verify").String(), nil) - if err != nil { - ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err) - w.WriteHeader(401) - return errors.New("unauthorized") - } - - originalURL := rOriginalHeaders(r, req) - - // Copy cookies from the incoming request - for _, cookie := range r.Cookies() { - req.AddCookie(cookie) - } - - // Making the verification request - resp, err := client.Do(req) - if err != nil { - ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err) - w.WriteHeader(401) - return errors.New("unauthorized") - } - - if resp.StatusCode != 200 { - redirectURL := autheliaURL.JoinPath() - - query := redirectURL.Query() - - query.Set("rd", originalURL.String()) - query.Set("rm", r.Method) - - http.Redirect(w, r, redirectURL.String(), http.StatusSeeOther) - return errors.New("unauthorized") - } - - return nil -} - -func rOriginalHeaders(r, req *http.Request) *url.URL { - if r.RemoteAddr != "" { - before, _, _ := strings.Cut(r.RemoteAddr, ":") - - if ip := net.ParseIP(before); ip != nil { - req.Header.Set("X-Forwarded-For", ip.String()) - } - } - - originalURL := &url.URL{ - Scheme: "http", - Host: r.Host, - Path: r.URL.Path, - RawPath: r.URL.RawPath, - } - - if r.TLS != nil { - originalURL.Scheme = "https" - } - - req.Header.Add("X-Forwarded-Method", r.Method) - req.Header.Add("X-Original-URL", originalURL.String()) - - return originalURL -} diff --git a/src/mod/auth/sso/authentik/authentik.go b/src/mod/auth/sso/authentik/authentik.go deleted file mode 100644 index 795b0b3..0000000 --- a/src/mod/auth/sso/authentik/authentik.go +++ /dev/null @@ -1,169 +0,0 @@ -package authentik - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "strings" - - "imuslab.com/zoraxy/mod/database" - "imuslab.com/zoraxy/mod/info/logger" - "imuslab.com/zoraxy/mod/utils" -) - -type AuthentikRouterOptions struct { - UseHTTPS bool //If the Authentik server is using HTTPS - AuthentikURL string //The URL of the Authentik server - Logger *logger.Logger - Database *database.Database -} - -type AuthentikRouter struct { - options *AuthentikRouterOptions -} - -// NewAuthentikRouter creates a new AuthentikRouter object -func NewAuthentikRouter(options *AuthentikRouterOptions) *AuthentikRouter { - options.Database.NewTable("authentik") - - //Read settings from database, if exists - options.Database.Read("authentik", "authentikURL", &options.AuthentikURL) - options.Database.Read("authentik", "useHTTPS", &options.UseHTTPS) - - return &AuthentikRouter{ - options: options, - } -} - -// HandleSetAuthentikURLAndHTTPS is the internal handler for setting the Authentik URL and HTTPS -func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - //Return the current settings - js, _ := json.Marshal(map[string]interface{}{ - "useHTTPS": ar.options.UseHTTPS, - "authentikURL": ar.options.AuthentikURL, - }) - - utils.SendJSONResponse(w, string(js)) - return - } else if r.Method == http.MethodPost { - //Update the settings - AuthentikURL, err := utils.PostPara(r, "authentikURL") - if err != nil { - utils.SendErrorResponse(w, "authentikURL not found") - return - } - - useHTTPS, err := utils.PostBool(r, "useHTTPS") - if err != nil { - useHTTPS = false - } - - //Write changes to runtime - ar.options.AuthentikURL = AuthentikURL - ar.options.UseHTTPS = useHTTPS - - //Write changes to database - ar.options.Database.Write("authentik", "authentikURL", AuthentikURL) - ar.options.Database.Write("authentik", "useHTTPS", useHTTPS) - - utils.SendOK(w) - } else { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - -} - -// HandleAuthentikAuth is the internal handler for Authentik authentication -// Set useHTTPS to true if your Authentik server is using HTTPS -// Set AuthentikURL to the URL of the Authentik server, e.g. Authentik.example.com -func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Request) error { - const outpostPrefix = "outpost.goauthentik.io" - client := &http.Client{} - - if ar.options.AuthentikURL == "" { - ar.options.Logger.PrintAndLog("Authentik", "Authentik URL not set", nil) - w.WriteHeader(500) - w.Write([]byte("500 - Internal Server Error")) - return errors.New("authentik URL not set") - } - protocol := "http" - if ar.options.UseHTTPS { - protocol = "https" - } - - authentikBaseURL := protocol + "://" + ar.options.AuthentikURL - //Remove tailing slash if any - authentikBaseURL = strings.TrimSuffix(authentikBaseURL, "/") - - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - reqUrl := scheme + "://" + r.Host + r.RequestURI - // Pass request to outpost if path matches outpost prefix - if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) { - req, err := http.NewRequest(r.Method, authentikBaseURL+r.RequestURI, r.Body) - if err != nil { - ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err) - w.WriteHeader(401) - return errors.New("unauthorized") - } - req.Header.Set("X-Original-URL", reqUrl) - req.Header.Set("Host", r.Host) - for _, cookie := range r.Cookies() { - req.AddCookie(cookie) - } - if resp, err := client.Do(req); err != nil { - ar.options.Logger.PrintAndLog("Authentik", "Unable to pass request to Authentik outpost", err) - w.WriteHeader(http.StatusInternalServerError) - return errors.New("internal server error") - } else { - defer resp.Body.Close() - for k := range resp.Header { - w.Header().Set(k, resp.Header.Get(k)) - } - w.WriteHeader(resp.StatusCode) - if _, err = io.Copy(w, resp.Body); err != nil { - ar.options.Logger.PrintAndLog("Authentik", "Unable to pass Authentik outpost response to client", err) - w.WriteHeader(http.StatusInternalServerError) - return errors.New("internal server error") - } - } - return nil - } - - //Make a request to Authentik to verify the request - req, err := http.NewRequest(http.MethodGet, authentikBaseURL+"/"+outpostPrefix+"/auth/nginx", nil) - if err != nil { - ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err) - w.WriteHeader(401) - return errors.New("unauthorized") - } - - req.Header.Set("X-Original-URL", reqUrl) - - // Copy cookies from the incoming request - for _, cookie := range r.Cookies() { - req.AddCookie(cookie) - } - - // Making the verification request - resp, err := client.Do(req) - if err != nil { - ar.options.Logger.PrintAndLog("Authentik", "Unable to verify", err) - w.WriteHeader(401) - return errors.New("unauthorized") - } - - if resp.StatusCode != 200 { - redirectURL := authentikBaseURL + "/" + outpostPrefix + "/start?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) - http.Redirect(w, r, redirectURL, http.StatusSeeOther) - return errors.New("unauthorized") - } - - return nil -} diff --git a/src/mod/auth/sso/forward/const.go b/src/mod/auth/sso/forward/const.go new file mode 100644 index 0000000..8fd1351 --- /dev/null +++ b/src/mod/auth/sso/forward/const.go @@ -0,0 +1,44 @@ +package forward + +import "errors" + +const ( + LogTitle = "Forward Auth" + + DatabaseTable = "auth_sso_forward" + + DatabaseKeyAddress = "address" + DatabaseKeyResponseHeaders = "responseHeaders" + DatabaseKeyRequestExcludedCookies = "requestExcludedCookies" + + HeaderXForwardedProto = "X-Forwarded-Proto" + HeaderXForwardedHost = "X-Forwarded-Host" + HeaderXForwardedFor = "X-Forwarded-For" + HeaderXForwardedURI = "X-Forwarded-URI" + HeaderXForwardedMethod = "X-Forwarded-Method" + + HeaderCookie = "Cookie" + + HeaderUpgrade = "Upgrade" + HeaderConnection = "Connection" + HeaderTransferEncoding = "Transfer-Encoding" + HeaderTE = "TE" + HeaderTrailers = "Trailers" + HeaderKeepAlive = "Keep-Alive" +) + +var ( + ErrInternalServerError = errors.New("internal server error") + ErrUnauthorized = errors.New("unauthorized") +) + +var ( + doNotCopyHeaders = []string{ + HeaderUpgrade, + HeaderConnection, + HeaderTransferEncoding, + HeaderTE, + HeaderTrailers, + HeaderKeepAlive, + } +) diff --git a/src/mod/auth/sso/forward/forward.go b/src/mod/auth/sso/forward/forward.go new file mode 100644 index 0000000..b9f23dc --- /dev/null +++ b/src/mod/auth/sso/forward/forward.go @@ -0,0 +1,294 @@ +package forward + +import ( + "encoding/json" + "io" + "net" + "net/http" + "strings" + + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/info/logger" + "imuslab.com/zoraxy/mod/utils" +) + +type AuthRouterOptions struct { + // Address of the forward auth endpoint. + Address string + + // ResponseHeaders is a list of headers to be copied from the response if provided by the forward auth endpoint to + // the request. + ResponseHeaders []string + + // RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream. + RequestExcludedCookies []string + + Logger *logger.Logger + Database *database.Database +} + +type AuthRouter struct { + client *http.Client + options *AuthRouterOptions +} + +// NewAuthRouter creates a new AuthRouter object +func NewAuthRouter(options *AuthRouterOptions) *AuthRouter { + options.Database.NewTable(DatabaseTable) + + //Read settings from database if available. + options.Database.Read(DatabaseTable, DatabaseKeyAddress, &options.Address) + + responseHeaders, requestExcludedCookies := "", "" + + options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders) + options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies) + + options.ResponseHeaders = strings.Split(responseHeaders, ",") + options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",") + + return &AuthRouter{ + client: &http.Client{ + CheckRedirect: func(r *http.Request, via []*http.Request) (err error) { + return http.ErrUseLastResponse + }, + }, + options: options, + } +} + +// HandleAPIOptions is the internal handler for setting the options. +func (ar *AuthRouter) HandleAPIOptions(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + ar.handleOptionsGET(w, r) + case http.MethodPost: + ar.handleOptionsPOST(w, r) + default: + ar.handleOptionsMethodNotAllowed(w, r) + } +} + +func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) { + js, _ := json.Marshal(map[string]interface{}{ + DatabaseKeyAddress: ar.options.Address, + DatabaseKeyResponseHeaders: ar.options.ResponseHeaders, + DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies, + }) + + utils.SendJSONResponse(w, string(js)) + + return +} + +func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request) { + // Update the settings + address, err := utils.PostPara(r, DatabaseKeyAddress) + if err != nil { + utils.SendErrorResponse(w, "address not found") + + return + } + + // These are optional fields. + responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders) + requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies) + + // Write changes to runtime + ar.options.Address = address + ar.options.ResponseHeaders = strings.Split(responseHeaders, ",") + ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",") + + // Write changes to database + ar.options.Database.Write(DatabaseTable, DatabaseKeyAddress, address) + ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders) + ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies) + + utils.SendOK(w) +} + +func (ar *AuthRouter) handleOptionsMethodNotAllowed(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + + return +} + +// HandleAuthProviderRouting is the internal handler for Forward Auth authentication. +func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.Request) error { + if ar.options.Address == "" { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + ar.options.Logger.PrintAndLog(LogTitle, "Address not set", nil) + + return ErrInternalServerError + } + + // Make a request to Authz Server to verify the request + req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + ar.options.Logger.PrintAndLog(LogTitle, "Unable to create request", err) + + return ErrInternalServerError + } + + // TODO: Add opt-in support for copying the request body to the forward auth request. + // TODO: Add support for customizing which headers are copied from the request to the forward auth request. + headerCopyExcluded(r.Header, req.Header, nil) + + // TODO: Add support for upstream headers. + rSetForwardedHeaders(r, req) + + // Make the Authz Request. + respForwarded, err := ar.client.Do(req) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + ar.options.Logger.PrintAndLog(LogTitle, "Unable to perform forwarded auth due to a request error", err) + + return ErrInternalServerError + } + + defer respForwarded.Body.Close() + + body, err := io.ReadAll(respForwarded.Body) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + ar.options.Logger.PrintAndLog(LogTitle, "Unable to read response to forward auth request", err) + + return ErrInternalServerError + } + + // Responses within the 200-299 range are considered successful and allow the proxy to handle the request. + if respForwarded.StatusCode >= http.StatusOK && respForwarded.StatusCode < http.StatusMultipleChoices { + // TODO: Add support for copying response headers to the response (in the user agent), not just the request. + + if len(ar.options.ResponseHeaders) != 0 { + // If the user has specified a list of cookies to be removed from the request, deterministically remove them. + headerCookieRedact(r, ar.options.RequestExcludedCookies) + } + + // Copy specific user-specified headers from the response of the forward auth request to the request sent to the + // upstream server/next hop. + headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseHeaders) + + return nil + } + + // Copy the response. + headerCopyExcluded(respForwarded.Header, w.Header(), nil) + + w.WriteHeader(respForwarded.StatusCode) + if _, err = w.Write(body); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + ar.options.Logger.PrintAndLog(LogTitle, "Unable to write response", err) + + return ErrInternalServerError + } + + return ErrUnauthorized +} + +func scheme(r *http.Request) string { + if r.TLS != nil { + return "https" + } + + return "http" +} + +func headerCookieRedact(r *http.Request, excluded []string) { + original := r.Cookies() + + if len(original) == 0 { + return + } + + var cookies []string + + for _, cookie := range original { + if stringInSlice(cookie.Name, excluded) { + continue + } + + cookies = append(cookies, cookie.String()) + } + + r.Header.Set(HeaderCookie, strings.Join(cookies, "; ")) +} + +func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) { + for key, values := range original { + // We should never copy the headers in the below list. + if stringInSliceFold(key, doNotCopyHeaders) { + continue + } + + if stringInSliceFold(key, excludedHeaders) { + continue + } + + destination[key] = append(destination[key], values...) + } +} + +func headerCopyIncluded(original, destination http.Header, includedHeaders []string) { + for key, values := range original { + // We should never copy the headers in the below list, even if they're in the list provided by a user. + if stringInSliceFold(key, doNotCopyHeaders) { + continue + } + + if !stringInSliceFold(key, includedHeaders) { + continue + } + + destination[key] = append(destination[key], values...) + } +} + +func stringInSlice(needle string, haystack []string) bool { + if len(haystack) == 0 { + return false + } + + for _, v := range haystack { + if needle == v { + return true + } + } + + return false +} + +func stringInSliceFold(needle string, haystack []string) bool { + if len(haystack) == 0 { + return false + } + + for _, v := range haystack { + if strings.EqualFold(needle, v) { + return true + } + } + + return false +} + +func rSetForwardedHeaders(r, req *http.Request) { + if r.RemoteAddr != "" { + before, _, _ := strings.Cut(r.RemoteAddr, ":") + + if ip := net.ParseIP(before); ip != nil { + req.Header.Set(HeaderXForwardedFor, ip.String()) + } + } + + req.Header.Set(HeaderXForwardedMethod, r.Method) + req.Header.Set(HeaderXForwardedProto, scheme(r)) + req.Header.Set(HeaderXForwardedHost, r.Host) + req.Header.Set(HeaderXForwardedURI, r.URL.Path) +} diff --git a/src/mod/dynamicproxy/authProviders.go b/src/mod/dynamicproxy/authProviders.go index 8099b55..1397a45 100644 --- a/src/mod/dynamicproxy/authProviders.go +++ b/src/mod/dynamicproxy/authProviders.go @@ -32,20 +32,16 @@ and return a boolean indicate if the request is written to http.ResponseWriter */ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool { requestHostname := r.Host - if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic { + + switch sep.AuthenticationProvider.AuthMethod { + case AuthMethodBasic: err := h.handleBasicAuthRouting(w, r, sep) if err != nil { h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") return true } - } else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia { - err := h.handleAutheliaAuth(w, r) - if err != nil { - h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") - return true - } - } else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthentik { - err := h.handleAuthentikAuth(w, r) + case AuthMethodForward: + err := h.handleForwardAuth(w, r) if err != nil { h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "") return true @@ -106,13 +102,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) return nil } -/* Authelia */ +/* Forward Auth */ -// Handle authelia auth routing -func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { - return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r) -} - -func (h *ProxyHandler) handleAuthentikAuth(w http.ResponseWriter, r *http.Request) error { - return h.Parent.Option.AuthentikRouter.HandleAuthentikAuth(w, r) +// Handle forward auth routing +func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error { + return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r) } diff --git a/src/mod/dynamicproxy/default.go b/src/mod/dynamicproxy/default.go index 39f2b50..eac6e21 100644 --- a/src/mod/dynamicproxy/default.go +++ b/src/mod/dynamicproxy/default.go @@ -17,12 +17,13 @@ import ( // GetDefaultAuthenticationProvider return a default authentication provider func GetDefaultAuthenticationProvider() *AuthenticationProvider { return &AuthenticationProvider{ - AuthMethod: AuthMethodNone, - BasicAuthCredentials: []*BasicAuthCredentials{}, - BasicAuthExceptionRules: []*BasicAuthExceptionRule{}, - BasicAuthGroupIDs: []string{}, - AutheliaURL: "", - UseHTTPS: false, + AuthMethod: AuthMethodNone, + BasicAuthCredentials: []*BasicAuthCredentials{}, + BasicAuthExceptionRules: []*BasicAuthExceptionRule{}, + BasicAuthGroupIDs: []string{}, + ForwardAuthURL: "", + ForwardAuthResponseHeaders: []string{}, + ForwardAuthRequestExcludedCookies: []string{}, } } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 29d20ad..21eddd5 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -9,13 +9,12 @@ package dynamicproxy */ import ( _ "embed" - "imuslab.com/zoraxy/mod/auth/sso/authentik" "net" "net/http" "sync" "imuslab.com/zoraxy/mod/access" - "imuslab.com/zoraxy/mod/auth/sso/authelia" + "imuslab.com/zoraxy/mod/auth/sso/forward" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" @@ -64,8 +63,7 @@ type RouterOption struct { PluginManager *plugins.Manager //Plugin manager for handling plugin routing /* Authentication Providers */ - AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication - AuthentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication + ForwardAuthRouter *forward.AuthRouter /* Utilities */ Logger *logger.Logger //Logger for reverse proxy requets @@ -141,11 +139,10 @@ type HeaderRewriteRules struct { type AuthMethod int const ( - AuthMethodNone AuthMethod = iota //No authentication required - AuthMethodBasic //Basic Auth - AuthMethodAuthelia //Authelia - AuthMethodOauth2 //Oauth2 - AuthMethodAuthentik + AuthMethodNone AuthMethod = iota //No authentication required + AuthMethodBasic //Basic Auth + AuthMethodForward //Forward + AuthMethodOauth2 //Oauth2 ) type AuthenticationProvider struct { @@ -155,9 +152,10 @@ type AuthenticationProvider struct { BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint - /* Authelia Settings */ - AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com - UseHTTPS bool //Whether to use HTTPS for the Authelia server + /* Forward Auth Settings */ + ForwardAuthURL string // Full URL of the Forward Auth endpoint. Example: https://auth.example.com/api/authz/forward-auth + ForwardAuthResponseHeaders []string // List of headers to copy from the forward auth server response to the request. + ForwardAuthRequestExcludedCookies []string // List of cookies to exclude from the request after sending it to the forward auth server. } // A proxy endpoint record, a general interface for handling inbound routing diff --git a/src/mod/update/v315/typedef315.go b/src/mod/update/v315/typedef315.go index 15c281a..4ec827b 100644 --- a/src/mod/update/v315/typedef315.go +++ b/src/mod/update/v315/typedef315.go @@ -59,7 +59,7 @@ type AuthProvider int const ( AuthProviderNone AuthProvider = iota AuthProviderBasicAuth - AuthProviderAuthelia + AuthProviderForward AuthProviderOauth2 ) diff --git a/src/reverseproxy.go b/src/reverseproxy.go index d00b606..8e60335 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -115,8 +115,7 @@ func ReverseProxtInit() { StatisticCollector: statisticCollector, WebDirectory: *path_webserver, AccessController: accessController, - AutheliaRouter: autheliaRouter, - AuthentikRouter: authentikRouter, + ForwardAuthRouter: forwardAuthRouter, LoadBalancer: loadBalancer, PluginManager: pluginManager, /* Utilities */ @@ -585,11 +584,9 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { if authProviderType == 1 { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic } else if authProviderType == 2 { - newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia + newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodForward } else if authProviderType == 3 { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2 - } else if authProviderType == 4 { - newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthentik } else { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone } diff --git a/src/start.go b/src/start.go index 0fadc05..7a1d0b4 100644 --- a/src/start.go +++ b/src/start.go @@ -15,7 +15,7 @@ import ( "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" - "imuslab.com/zoraxy/mod/auth/sso/authelia" + "imuslab.com/zoraxy/mod/auth/sso/forward" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database/dbinc" "imuslab.com/zoraxy/mod/dockerux" @@ -143,18 +143,10 @@ func startupSequence() { } //Create authentication providers - autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{ - UseHTTPS: false, // Automatic populate in router initiation - AutheliaURL: "", // Automatic populate in router initiation - Logger: SystemWideLogger, - Database: sysdb, - }) - - authentikRouter = authentik.NewAuthentikRouter(&authentik.AuthentikRouterOptions{ - UseHTTPS: false, // Automatic populate in router initiation - AuthentikURL: "", // Automatic populate in router initiation - Logger: SystemWideLogger, - Database: sysdb, + forwardAuthRouter = forward.NewAuthRouter(&forward.AuthRouterOptions{ + Address: "", + Logger: SystemWideLogger, + Database: sysdb, }) //Create a statistic collector diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 1a9d653..69a773b 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -185,9 +185,8 @@ ${subd.AuthenticationProvider.AuthMethod == 0x1?` Basic Auth`:``} - ${subd.AuthenticationProvider.AuthMethod == 0x2?` Authelia`:``} - ${subd.AuthenticationProvider.AuthMethod == 0x3?` Oauth2`:``} - ${subd.AuthenticationProvider.AuthMethod == 0x4?` Authentik`:``} + ${subd.AuthenticationProvider.AuthMethod == 0x2?` Forward Auth`:``} + ${subd.AuthenticationProvider.AuthMethod == 0x3?` OAuth2`:``} ${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"
":""} ${subd.RequireRateLimit?` Rate Limit @ ${subd.RateLimit} req/s`:``} ${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`No Special Settings`:""} @@ -393,13 +392,7 @@
- -
-
-
-
- - +
diff --git a/src/web/components/sso.html b/src/web/components/sso.html index 820735a..0440e33 100644 --- a/src/web/components/sso.html +++ b/src/web/components/sso.html @@ -14,44 +14,43 @@
-

Authelia

-

Configuration settings for Authelia authentication provider.

- +

Forward Auth

+

Configuration settings for the Forward Auth provider.

+

The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:

+ +

Example authorization servers that support this:

+
- - - Example: auth.example.com + + + The full remote address or URL of the authorization servers forward auth endpoint. Example: https://auth.example.com/authz/forward-auth
-
-
- - - Check this if your authelia server uses HTTPS +
+
+ + Advanced Options
-
- - -
-
-
-

Authentik

-

Configuration settings for Authentik authentication provider.

- -
-
- - - Example: auth.example.com -
-
-
- - - Check this if your Authentik server uses HTTPS +
+
+ + + Comma separated list of case-insensitive headers to copy from the authorization servers response, to the request to the backend. Example: Remote-User,Remote-Groups,Remote-Email,Remote-Name +
+
+ + + Comma separated list of case-sensitive cookie names to exclude from the request to the backend. Example: authelia_session,another_session +
-
- +

+
@@ -60,24 +59,13 @@ diff --git a/src/web/snippet/pluginstore.html b/src/web/snippet/pluginstore.html index e7205f0..fabb83b 100644 --- a/src/web/snippet/pluginstore.html +++ b/src/web/snippet/pluginstore.html @@ -58,28 +58,28 @@
+ -->